Skip to main content
The MCP Connections API lets an agent run call a third-party (non-Narrative) Model Context Protocol server that requires user authorization. The platform performs the OAuth 2.1 handshake against the external authorization server — including RFC 7591 Dynamic Client Registration (DCR) and PKCE — persists the tokens KMS-encrypted, and attaches (and refreshes) the bearer server-side on every tool call. The token is never returned by the API, never appears in a request payload, and never lands in a run’s effective_config or history. This page is a complete reference: when to use it, the full connection lifecycle, the four endpoints, how a connection plugs into an agent run, and how connections behave with respect to ownership and errors.
Scope: per-user, not per-company. A connection is bound to the (company_id, user_id) pair on the bearer token that created it. Peers in the same company cannot see, list, fetch, delete, or reference each other’s connections — every not-owned access returns 404 identical to a nonexistent id, so ids cannot be probed across users.

When to use this

You need this API only when the MCP server you want to use requires the end user’s own OAuth authorization — for example a third-party developer platform’s MCP that returns data scoped to the caller’s account. You do not need it for:
  • Narrative-owned MCP servers (e.g. the Data Collaboration MCP Server). The platform auto-authenticates these — just list the server in mcp_servers[] with no connection_id.
  • Fully public MCP servers that expose their tools without authentication. Same as above — omit connection_id.
If unsure, run the conversation first without a connection_id. If the server returns 401 Unauthorized on tools/list, you need a connection.

Requirements the external server must meet

The connection flow only works against a server that speaks the OAuth 2.1 dialect the platform implements. During POST /mcp-connections the platform performs these checks and returns 400 with the appropriate detail on any miss:
  • The server must publish OAuth protected-resource metadata (RFC 9728), with an authorization_servers entry pointing at its issuer.
  • The authorization server must publish metadata via RFC 8414 (.well-known/oauth-authorization-server) or OIDC discovery (.well-known/openid-configuration) — either is accepted.
  • The authorization server must advertise S256 in code_challenge_methods_supported. Servers that advertise PKCE methods but omit S256 are rejected up front — the platform never falls back to plain. Servers that omit the field entirely proceed on the mandated baseline (S256).
  • The authorization server must expose a registration endpoint (RFC 7591). Servers that only support pre-registered clients cannot be connected this way.
The platform registers as a public PKCE client (no client secret to store) with a fixed redirect_uri pointing at GET /mcp-connections/callback.

Connection lifecycle

A connection moves through three statuses. Only connected connections are usable by an agent run. Reconnecting the same server URL as the same user replaces the previous row — there is never more than one connection per (company_id, user_id, server_url) triple.

End-to-end flow

1

Start the connection

POST /mcp-connections with the server URL and a routing alias. The platform discovers the server, verifies PKCE support, registers a client, persists a pending row, and returns an authorization_url.
2

User consents

Direct the user to the returned authorization_url in a browser. They log in to the external service and grant access.
3

Callback completes the connection

The external authorization server redirects the browser to GET /mcp-connections/callback?code=...&state=.... The platform exchanges the code (with the stored PKCE verifier), validates the token with a tools/list call, encrypts the access + refresh tokens under a dedicated KMS key, and flips the row to connected. The user sees a short plain-text confirmation.
4

Use it in an agent run

Reference the connection_id from the matching entry in mcp_servers[] when creating a conversation. The platform resolves the bearer server-side at every tools/list and tools/call, refreshing it if the access token has expired.

API endpoints

All endpoints live under /mcp-connections. Every endpoint except the callback requires a Bearer token with agent_conversations read/write permission.

POST /mcp-connections

Starts the OAuth flow. Request body:
url
string (uri)
required
Absolute http(s) URL of the external MCP server. Must satisfy the requirements listed above (RFC 9728 metadata, RFC 7591 registration, S256 PKCE).
alias
string
required
Short routing prefix for this server’s tools inside an agent run. 1–64 characters of [A-Za-z0-9_]. No dashes — the dash is reserved as the {alias}-{tool} routing separator at tool-call time.
Response (201 Created):
connection_id
string (uuid)
Id of the pending connection. You reference this from mcp_servers[].connection_id once the connection reaches connected.
authorization_url
string (uri)
The external authorization server’s consent URL, with client_id, redirect_uri, state, the PKCE code_challenge (S256), and the resource indicator already applied. Open it in the user’s browser.
Common 400 causes:

GET /mcp-connections

Returns the caller’s connections. Not paginated; empty state is { "connections": [] }.
The status view is credential-free — the registered client secret and the encrypted access/refresh tokens are never returned by any endpoint.

GET /mcp-connections/{id}

Returns one connection using the same status shape. Returns 404 for both nonexistent ids and ids owned by a different user — the two are indistinguishable by design.

DELETE /mcp-connections/{id}

Removes the connection and its stored ciphertext, and cascades any in-flight authorization flow state. Returns 204 on success and 404 for missing or not-owned ids.
After deletion, agent runs that reference this connection_id can no longer resolve a token for that server — the next tool call fails auth. Re-connect via POST /mcp-connections to restore access. The new connection has a different connection_id; update your mcp_servers[] entries.

GET /mcp-connections/callback

The OAuth redirect URI. Called by the user’s browser after they consent; carries ?code=...&state=... from the external authorization server. No bearer token — the one-time, single-use state (bound to a stored PKCE code_verifier) is the credential. On success the platform:
  1. Looks up the pending flow by state.
  2. Exchanges the code at the server’s token endpoint with the PKCE verifier and resource indicator.
  3. Validates the new access token by calling tools/list on the server.
  4. Encrypts the access + refresh tokens under the dedicated KMS key (AAD bound to connection_id + user_id — a row cannot be decrypted against a different connection) and flips the connection to connected.
  5. Consumes the one-time state.
The response is a short text/plain page — for example, MCP connection established. You can close this window. — for the user to close in their browser. Failures return 400 (bad or expired state, exchange error, validation error) or 404 (the connection the state referred to no longer exists).

Using a connection in an agent run

Once a connection is connected, wire it into a conversation by adding connection_id to the matching entry in mcp_servers[]:
At tools/list (per-run discovery) and every tools/call, the platform:
  • Loads the connection by id.
  • Verifies the connection belongs to the run’s (company_id, user_id). A connection owned by another user is reported as if it does not exist (the tool call fails as if the connection is missing), so runs cannot reference or probe other users’ connections.
  • Decrypts the access token. If it is within its expiry window it is used as-is; if expired and a refresh token is available, the platform runs a refresh exchange, encrypts and stores the new tokens, and uses the fresh access token.
  • Attaches the token as Authorization: Bearer ... on the outbound request to the MCP server.
The bearer never appears in the conversation payload, the run’s effective_config, the run’s message history, or GET /agents/runs/{id} responses. For the shape of the mcp_servers[] entry, see the Agent Conversations reference.

Security model

  • Ciphertext-only at rest. Access and refresh tokens are encrypted client-side (envelope encryption) under a dedicated KMS key (alias/external-mcp-tokens-<stage>) before being persisted. The database never sees plaintext.
  • AAD binding to (connection_id, user_id). A ciphertext row cannot be decrypted against a different connection or user — KMS itself enforces the binding at decrypt time.
  • No client secret at rest. The platform registers as a public PKCE client — there is no client secret to store, only the ephemeral PKCE verifier that lives with the pending flow state and is consumed on the callback.
  • S256 only. The platform never sends plain PKCE challenges. Servers that don’t support S256 are rejected at POST /mcp-connections.
  • One-shot state. The state issued in authorization_url is unguessable, bound to the pending row, and consumed on first use.
  • Per-user isolation. Every connection is scoped to the creating user; cross-user access is indistinguishable from a missing id (404).

See also