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 noconnection_id. - Fully public MCP servers that expose their tools without authentication. Same as
above — omit
connection_id.
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. DuringPOST /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_serversentry 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
S256incode_challenge_methods_supported. Servers that advertise PKCE methods but omitS256are rejected up front — the platform never falls back toplain. 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.
redirect_uri pointing at GET /mcp-connections/callback.
Connection lifecycle
A connection moves through three statuses. Onlyconnected 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:
Absolute
http(s) URL of the external MCP server. Must satisfy the requirements listed
above (RFC 9728 metadata, RFC 7591 registration, S256 PKCE).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.201 Created):
Id of the pending connection. You reference this from
mcp_servers[].connection_id once
the connection reaches connected.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.GET /mcp-connections
Returns the caller’s connections. Not paginated; empty state is { "connections": [] }.
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.
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:
- Looks up the pending flow by
state. - Exchanges the
codeat the server’s token endpoint with the PKCE verifier and resource indicator. - Validates the new access token by calling
tools/liston the server. - 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 toconnected. - Consumes the one-time
state.
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 isconnected, wire it into a conversation by adding connection_id to
the matching entry in mcp_servers[]:
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.
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.
S256only. The platform never sendsplainPKCE challenges. Servers that don’t supportS256are rejected atPOST /mcp-connections.- One-shot
state. Thestateissued inauthorization_urlis 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
- Agent Conversations reference — how
mcp_servers[]andconnection_idplug into a run. - Data Collaboration MCP Server — the Narrative-owned
MCP server, which is auto-authenticated and needs no
mcp-connectionsflow. - MCP Discovery Failed
— the run-time error surfaced when a
tools/listagainst a registered server fails, including servers reached via an external OAuth connection.

