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. 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
External servers ship behind the mcp_external_servers per-org feature flag. Until enabled the connect endpoints return 403.
Connecting a server
The connect surface ships as an API today; a dashboard view is in progress. The same fields will appear there when it lands:
| Field | Detail |
|---|
| Name | Friendly name shown wherever the server appears. |
| Slug | Lowercase, alphanumeric + underscore, max 32 chars (e.g. internal_billing). Must start with a letter. Becomes the namespace prefix on every discovered tool. |
| URL | The MCP server’s HTTP JSON-RPC endpoint. Must be HTTPS in production. Max 2048 chars. |
| Auth method | none, bearer, or oauth2. |
| Credentials | A bearer token (for bearer), or an access_token plus optional refresh_token (for oauth2). Stored encrypted at rest. |
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
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:
{
"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, created_at. |
| Refresh discovery | PATCH /api/integrations/mcp/servers/{id} | Re-runs initialize + tools/list synchronously and updates the snapshot. |
| Disconnect | DELETE /api/integrations/mcp/servers/{id} | Soft-delete (see below). |
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.
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.
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.