Skip to main content

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:
FieldDetail
NameFriendly name shown wherever the server appears.
SlugLowercase, alphanumeric + underscore, max 32 chars (e.g. internal_billing). Must start with a letter. Becomes the namespace prefix on every discovered tool.
URLThe MCP server’s HTTP JSON-RPC endpoint. Must be HTTPS in production. Max 2048 chars.
Auth methodnone, bearer, or oauth2.
CredentialsA 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

ActionEndpointNotes
ListGET /api/integrations/mcp/serversReturns id, slug, name, url, auth_method, status, last_discovered_at, last_error, discovered_tools, created_at.
Refresh discoveryPATCH /api/integrations/mcp/servers/{id}Re-runs initialize + tools/list synchronously and updates the snapshot.
DisconnectDELETE /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:
ProtectionWhat it catches
HTTPS / loopback onlyThe URL is rejected at connect time if it’s not https:// (or loopback in dev).
SSRF allowlistURLs targeting localhost, RFC 1918 private networks, link-local addresses, and cloud metadata IPs are rejected.
DNS-aware re-checkBoth 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 fetchredirect: "manual" on every outgoing fetch so a 3xx from the upstream cannot bounce us into a network the SSRF check just rejected.
Credential encryptionThe bearer token / OAuth tokens are stored encrypted with per-field GCM authentication. Disconnecting wipes the credentials.
Per-call timeout15 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:
SurfaceNames
Synthetic integrationmcp_internal_billing (shows as a card under Agents > Actions > Other integrations)
Action idsmcp.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:
RuleWhy
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 serverA 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

LimitValue
Discovery timeout15 seconds
Tool call timeout30 seconds
Tool name length1-64 chars
Tool name shape[a-zA-Z0-9_-]+
URL length2048 chars
Bearer token length8000 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.