Skip to main content

Transport

AspectValue
ProtocolModel Context Protocol 2025-03-26
TransportHTTP + JSON-RPC 2.0 (single endpoint, stateless per request)
EndpointPOST https://app.haloagents.ai/api/mcp
AuthOAuth 2.1 with PKCE, Authorization: Bearer halo_at_*
Server namehalo-agents
Server version1.0.0

Discovery URLs

A bare GET /api/mcp returns a small descriptor with all the URLs an MCP-aware client needs:
{
  "name": "halo-agents",
  "version": "1.0.0",
  "protocolVersion": "2025-03-26",
  "transport": "http+jsonrpc",
  "methods": ["initialize", "ping", "tools/list", "tools/call"],
  "authentication": {
    "type": "oauth2",
    "authorization_server": "https://app.haloagents.ai/.well-known/oauth-authorization-server",
    "protected_resource_metadata": "https://app.haloagents.ai/.well-known/oauth-protected-resource"
  },
  "mcp_url": "https://app.haloagents.ai/api/mcp"
}
You can also walk the standard discovery flow:
URLRFCWhat it returns
/.well-known/oauth-protected-resourceRFC 9728The resource id and the authorization-server issuer (https://app.haloagents.ai) to talk to.
/.well-known/oauth-authorization-serverRFC 8414The full OAuth metadata: authorization_endpoint, token_endpoint, registration_endpoint, revocation_endpoint, supported scopes, supported PKCE methods.
A 401 from /api/mcp carries a WWW-Authenticate: Bearer realm="halo-mcp", resource_metadata="...", as_uri="..." header pointing at the same documents, so a client doesn’t need to know the URLs ahead of time.

OAuth 2.1 endpoints

EndpointMethodRFCNotes
/api/mcp/oauth/registerPOSTRFC 7591Dynamic Client Registration. Public; rate-limited at 10 registrations per IP per hour.
/api/mcp/oauth/authorizeGETRFC 6749 §4.1Authorization-code + PKCE entry point. Bounces unauthenticated users through /signin.
/api/mcp/oauth/consentPOST(Halo)Internal. The consent UI’s form posts here to issue the authorization code.
/api/mcp/oauth/tokenPOSTRFC 6749 §3.2Token + refresh grants. grant_type must be authorization_code or refresh_token.
/api/mcp/oauth/revokePOSTRFC 7009Idempotent. Always returns 200.
Supported grants: authorization_code, refresh_token. Supported PKCE methods: S256 only. Supported response types: code. Supported token endpoint auth methods: none (public clients), client_secret_basic, client_secret_post.

Token formats

TokenPrefixLifetime
Access tokenhalo_at_1 hour
Refresh tokenhalo_rt_Long-lived, rotates on every use
Authorization codehalo_ac_5 minutes, single-use
All three are 256-bit random values, base64url-encoded after the prefix. Halo persists only the SHA-256 hash; on every request the bearer is hashed and looked up. There is no JWT, no signature verification, no jwks_uri.

JSON-RPC methods

initialize

Capabilities handshake. Required as the first call after a fresh OAuth. Request:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize"
}
Response:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "serverInfo": { "name": "halo-agents", "version": "1.0.0" },
    "capabilities": {
      "tools": { "listChanged": false }
    }
  }
}

ping

Liveness check. Returns an empty object on success.
{ "jsonrpc": "2.0", "id": 2, "method": "ping" }

tools/list

Returns the tool descriptors the token’s scopes (and the org’s connections + agent toggles) unlock. Request:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/list" }
Response:
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      {
        "name": "search_docs",
        "description": "Search the public Halo Agents documentation...",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": { "type": "string" },
            "page_size": { "type": "number" }
          },
          "required": ["query"],
          "additionalProperties": false
        }
      },
      {
        "name": "linear.create_bug",
        "description": "File a bug ticket in Linear...",
        "inputSchema": { "type": "object", "properties": { "...": "..." } },
        "annotations": { "destructiveHint": true }
      }
    ]
  }
}
Tool names are namespaced as <integration>.<action> for native integrations, mcp.<slug>.<tool> for connected external MCP servers, and bare names (search_docs, read_doc) for the docs proxy tools. The annotations.destructiveHint: true flag is set on tools that mutate external state.

tools/call

Invoke a tool. Wrap arguments under params.arguments; pass session metadata under params._meta. Request:
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "hubspot.create_deal",
    "arguments": { "name": "ACME renewal", "amount": 50000 },
    "_meta": {
      "bound_end_user_id": "u_abc123"
    }
  }
}
Response (success):
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"deal_id\": \"deal_xyz\", ...}"
      }
    ],
    "isError": false
  }
}
The result is always wrapped in a single text content block. Structured tool output is JSON-stringified (with 2-space indent) inside that block.

Error codes

Standard JSON-RPC plus a couple of Halo-specific codes:
CodeMeaning
-32700Parse error (invalid JSON body).
-32600Invalid Request (bad JSON-RPC envelope).
-32601Method not found, or unknown tool name on tools/call.
-32602Invalid params (validation failure on tool args).
-32603Internal error.
-32001Unauthorized (no token, bad token, expired token). Returned with HTTP 401 + WWW-Authenticate.
-32002Forbidden (scope denied, destructive blocked, integration disabled, agent disabled, rate limited, feature flag off).
Tool-call failures preserve the original error code where possible: unauthorized-32002, validation-32602, not_found-32601, anything else → -32603.

Rate limits

MethodPer-token cap
tools/list60 per minute
tools/call120 per minute
Other60 per minute
Caps are per-token, on top of the IP-based dashboard limit. Hitting one returns HTTP 429 with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Per-action and per-org caps still apply on top. DCR is rate-limited at 10 registrations per IP per hour.

Session metadata (_meta)

The MCP _meta envelope on tools/call lets clients pass per-call context that isn’t part of the tool’s input schema:
KeyValueEffect
bound_end_user_idHalo customer idThe tool runs on behalf of that customer (resolved per call against your org).
bound_emailEmail stringSame as above but resolved by email lookup. Use when the client only knows the email.
The binding is re-validated server-side on every call. A stale id, a forged id, or an id from a foreign tenant is rejected.

Helpful headers

Halo sets these on relevant responses:
HeaderWhen
WWW-Authenticate: Bearer realm="halo-mcp", resource_metadata="...", as_uri="..."On 401 responses from /api/mcp.
Cache-Control: public, max-age=300On the .well-known/* discovery documents.
Cache-Control: no-storeOn /oauth/token responses (per RFC 6749 §5.1).
Retry-After, X-RateLimit-*On 429 responses.

A bare-bones client

A minimal curl walkthrough once you have an access token:
TOKEN="halo_at_..."
URL="https://app.haloagents.ai/api/mcp"

# Initialize
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

# List tools
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# Call a tool
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":3,"method":"tools/call",
    "params": {
      "name":"search_docs",
      "arguments": {"query":"escalation policy","page_size":5}
    }
  }'