Transport
Two surfaces
/api/mcp is the canonical server: the URL every client is documented against and the one listed in the Claude Connectors Directory. It is auth-first: every request without a credential, initialize included, is answered with 401 and the WWW-Authenticate challenge below. That is how Claude detects that sign-in is required when you add the connector.
/api/mcp/chatgpt is the same server as the ChatGPT / Codex plugin connects to it. The only difference is that initialize, ping and tools/list answer without a credential (the full tool catalogue, each tool declaring the OAuth scopes that unlock it in securitySchemes), because OpenAI’s plugin portal scans tools before an account is linked. tools/call always requires a token. A token issued for a grant that carried an RFC 8707 resource is bound to that surface and refused on the other; grants from clients that omit resource (allowed for compatibility) are accepted on both.
GET on either endpoint returns 405 (there is no SSE stream to open).
Discovery URLs
Walk the standard discovery flow:
A 401 from either endpoint carries a
WWW-Authenticate: Bearer realm="halo-mcp", resource_metadata="...", as_uri="...", error="invalid_token" header pointing at the right documents, so a client doesn’t need to know the URLs ahead of time.
OAuth 2.1 endpoints
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.
Clients identify themselves in one of two ways:
- Dynamic Client Registration (RFC 7591):
POST /api/mcp/oauth/registeronce, then use the returnedclient_id. - Client ID Metadata Documents: the
client_idis the https URL of the client’s own metadata document (ChatGPT useshttps://chatgpt.com/oauth/client.json, Claude Codehttps://claude.ai/oauth/claude-code-client-metadata). Halo fetches the document at/authorize, checks that it names itself and that every redirect URI is on the document’s origin (or loopback), and accepts it as a public client. No registration call is needed.
http://localhost/..., http://127.0.0.1/...) match with the port ignored (RFC 8252 §7.3), so native clients can bind an ephemeral port per authorization. Every authorization response, success or error, carries iss (RFC 9207).
Token formats
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:
ping
Liveness check. Returns an empty object on success.
tools/list
Returns the tool descriptors the token’s scopes (and the org’s connections + agent toggles) unlock.
Request:
<integration>.<action> for native integrations, mcp.<slug>.<tool> for connected external MCP servers, and prefixed bare names for the built-in tool families: search_docs / read_doc (docs proxy), setup_* (setup helpers), data_* (workspace-data reads plus the data_update_user / data_update_company writes), inbox_* (ticket replies, notes, triage, outreach), automation_* (campaign read / create / edit / preview / activate), and media_* (image upload / list for email content). The annotations.destructiveHint: true flag is set on tools that mutate external state, and readOnlyHint: true on pure reads. The initialize result also carries an instructions string describing the markdown content model and the build flow, which MCP clients pass to the model as context.
tools/call
Invoke a tool. Wrap arguments under params.arguments; pass session metadata under params._meta.
Request:
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:
Tool-call failures preserve the original error code where possible:
unauthorized → -32002, validation → -32602, not_found → -32601, anything else → -32603.
Rate limits
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:
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:A bare-bones client
A minimalcurl walkthrough once you have an access token: