> ## Documentation Index
> Fetch the complete documentation index at: https://docs.haloagents.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connectors (External MCP Servers)

> Connect a third-party MCP server to your org and its tools become available as agent actions, alongside Halo's built-in integrations.

## What this is

The other side of MCP. Instead of an external client calling Halo, **Halo calls an external MCP server** and treats its tools the same way it treats a native integration's actions. In the dashboard these are called **Connectors**. Once connected, the discovered tools show up under **AI Agents > \[Agent] > Actions** as enable-able toggles.

Use it for:

* An internal company API exposed as an MCP server (your billing system, your provisioning service)
* A vendor's MCP server (anything from the public MCP registry)
* A small wrapper over a domain-specific data source you don't want to ingest into Halo's knowledge base

<Note>
  Connectors are available to every org by default. Your Halo admin can disable them per-workspace with the `mcp_external_servers` feature flag, in which case the connect endpoints return 403 and the dashboard surface is hidden.
</Note>

## Connecting a server from the dashboard

Go to **Integrations > Connectors** (also available under **Settings > Integrations**) and click **Add connector**:

| Field              | Detail                                                                                                                                                         |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**           | Friendly name shown wherever the server appears. The slug is auto-derived from it as you type.                                                                 |
| **Slug**           | Lowercase, alphanumeric + underscore, max 32 chars (e.g. `internal_billing`). Must start with a letter. Becomes the namespace prefix on every discovered tool. |
| **Server URL**     | The MCP server's streamable HTTP endpoint (e.g. `https://mcp.example.com/mcp`). Must be HTTPS in production. Max 2048 chars.                                   |
| **Authentication** | `None`, `Bearer token`, or `OAuth2 tokens`.                                                                                                                    |
| **Credentials**    | A bearer token (for bearer auth), or an access token plus optional refresh token (for OAuth2). Stored encrypted at rest.                                       |

Halo connects and discovers the server's tools immediately. If discovery fails (bad URL, bad credentials, unreachable server), the connector is still saved with the error shown on its card so you can fix and **Refresh** without re-entering everything.

Each connector card shows the server's status, its discovered tools (with destructive operations flagged), and actions to re-run discovery or disconnect. Connectors display the server's own branding when it declares an icon, falling back to the server's favicon, then a generic mark.

The `integrations:manage` role is required for any mutating call (connect, disconnect, refresh). Read-only members with `integrations:view` can list connected servers.

### Connect via API

```bash theme={null}
curl -X POST https://app.haloagents.ai/api/integrations/mcp/servers \
  -H "Authorization: Bearer <dashboard session>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Internal Billing",
    "slug": "internal_billing",
    "url": "https://mcp.internal.example.com/jsonrpc",
    "auth_method": "bearer",
    "credentials": { "token": "<upstream-token>" }
  }'
```

Halo runs `initialize` + `tools/list` synchronously on connect, so the response includes the discovered tool count (or a clear error message) in the same call:

```json theme={null}
{
  "id": "mcp_srv_abc123",
  "status": "connected",
  "tool_count": 7,
  "error": null
}
```

A failed discovery leaves the row with `status: "error"` and `last_error` populated, so you can fix the URL or credentials and retry without re-entering everything.

### List, refresh, disconnect

| Action                | Endpoint                                    | Notes                                                                                                                                                                                          |
| --------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **List**              | `GET /api/integrations/mcp/servers`         | Returns id, slug, name, url, auth\_method, status, last\_discovered\_at, last\_error, discovered\_tools, icon\_url, created\_at, plus a top-level `enabled` reflecting the org's feature flag. |
| **Refresh discovery** | `PATCH /api/integrations/mcp/servers/{id}`  | Re-runs the handshake + `tools/list` synchronously and updates the snapshot.                                                                                                                   |
| **Disconnect**        | `DELETE /api/integrations/mcp/servers/{id}` | Soft-delete (see below).                                                                                                                                                                       |

## Protocol compatibility

Halo speaks the MCP **streamable HTTP** transport and works with both stateless and stateful servers:

| Behavior              | Detail                                                                                                                                                                        |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Handshake**         | Every operation runs `initialize` followed by `notifications/initialized`, then the actual request, as the spec requires.                                                     |
| **Stateful sessions** | When the server issues an `Mcp-Session-Id` on initialize, Halo echoes it (and the negotiated `MCP-Protocol-Version`) on every follow-up request in that operation.            |
| **SSE responses**     | Servers that respond with `text/event-stream` (including ones that interleave notifications before the response message) are parsed correctly; plain JSON responses work too. |
| **Protocol version**  | Halo proposes `2025-03-26` and accepts the server's negotiated version.                                                                                                       |

## Server icons

Servers that declare icons in their `initialize` response (`serverInfo.icons`, SEP-973) get their own branding on the connector card and in the agent Actions tab. Because icons are input from an external server, Halo validates hard before persisting anything:

* `https` icon URLs must be same-site with the MCP server's own host and pass the same SSRF checks as the server URL itself.
* Inline icons must be base64 raster data URIs (PNG, JPEG, WebP, or GIF). SVG is rejected since it can embed script.
* Icons are re-derived on every discovery run, so a server that changes or drops its branding never leaves a stale icon behind.

If no declared icon passes validation, the card falls back to the server origin's favicon, then a generic MCP mark.

## Security

External MCP servers run inside Halo's request path, so the connect flow is hardened:

| Protection                | What it catches                                                                                                                                                                  |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **HTTPS / loopback only** | The URL is rejected at connect time if it's not `https://` (or loopback in dev).                                                                                                 |
| **SSRF allowlist**        | URLs targeting localhost, RFC 1918 private networks, link-local addresses, and cloud metadata IPs are rejected.                                                                  |
| **DNS-aware re-check**    | Both A and AAAA records are resolved at connect time and on every tool call. A hostname that points at a private IP is rejected even if the literal URL passes the static check. |
| **No-redirect fetch**     | `redirect: "manual"` on every outgoing fetch so a 3xx from the upstream cannot bounce us into a network the SSRF check just rejected.                                            |
| **Credential encryption** | The bearer token / OAuth tokens are stored encrypted with per-field GCM authentication. Disconnecting wipes the credentials.                                                     |
| **Per-call timeout**      | 15 second timeout on discovery, 30 second timeout on tool calls.                                                                                                                 |

Discovery is also rerun on a schedule (an Inngest cron) so a tool added or removed on the upstream server eventually shows up in Halo without manual intervention. You can force a refresh from the server's row by clicking **Refresh discovery**.

## How discovered tools surface

Each connected server contributes a synthetic integration to the registry. The integration id is `mcp_<slug>` and each tool's action id is `mcp.<slug>.<tool_name>`.

Example. You connect a server with `slug: internal_billing` that exposes `get_invoice` and `void_invoice`. Halo creates:

| Surface                   | Names                                                                                |
| ------------------------- | ------------------------------------------------------------------------------------ |
| **Synthetic integration** | `mcp_internal_billing` (shows as a card under Agents > Actions > Other integrations) |
| **Action ids**            | `mcp.internal_billing.get_invoice`, `mcp.internal_billing.void_invoice`              |

Enable them on the agent like any other action. The agent will call them during conversations whenever the tool descriptions match the user's intent.

## Tool-name rules

Halo filters discovered tools to enforce two rules:

| Rule                                 | Why                                                                                                                                               |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name matches `^[a-zA-Z0-9_-]{1,64}$` | Same shape as static action ids; lets the audit log + per-action rate limiter address them uniformly.                                             |
| Names are unique within a server     | A duplicate name from the upstream is dropped (the second one in `tools/list`) so the registry can't have two synthetic actions with the same id. |

Bad names are dropped silently with a warn log; the rest of the server's tools are kept. So a single broken tool doesn't take down the whole integration.

## Customer binding

Static actions know who the customer is via `for_customer_email` (passed by the AI SDK). External MCP tools don't get that override. Instead, the customer binding flows through the MCP session's `_meta.bound_end_user_id` envelope when Halo calls the upstream server, so:

* An external MCP server that wants to scope its tools to "the user this conversation is about" should read `_meta.bound_end_user_id` from each `tools/call`.
* An external MCP server that doesn't care (e.g. a generic search API) can ignore `_meta`.

## Disconnecting

`DELETE /api/integrations/mcp/servers/{id}` soft-deletes the row, wipes the encrypted credentials, and removes every discovered tool from the registry. The row itself is kept for audit-log integrity so historical `action_executions` rows still resolve to a real server entity.

Reconnecting always re-takes credentials from you, so a wiped row can't be revived without re-entering them.

## Limits

| Limit               | Value                             |
| ------------------- | --------------------------------- |
| Discovery timeout   | 15 seconds                        |
| Tool call timeout   | 30 seconds                        |
| Tool name length    | 1-64 chars                        |
| Tool name shape     | `[a-zA-Z0-9_-]+`                  |
| URL length          | 2048 chars                        |
| Bearer token length | 8000 chars (per credential field) |

Per-org per-action rate limits apply to MCP tools the same way they apply to native integration actions, so a runaway agent can't spam an upstream server.
