Finish connecting an external MCP server
Completes a connection started by POST /mcp-connections, after the user has consented and the
authorization server has redirected their browser through GET /mcp-connections/callback.
The Narrative app calls this from its callback page, so normally you don’t call it yourself. It is part of the public API because the connection flow can also be driven by an integration that handles the redirect itself.
What this endpoint actually does
- Looks up the authorization flow by
state(400 if unknown or expired). - Checks the connection behind that flow belongs to the calling user (404 otherwise).
- Exchanges the stored authorization code at the server’s token endpoint, with the flow’s PKCE
code_verifierand the resource indicator. - Validates the new token with a
tools/listcall against the MCP server. - Stores the encrypted tokens, flips the connection to
connected, and consumes the flow.
Returns the connection’s status view, the same shape as GET /mcp-connections/{id}.
Only the user who started the flow can complete it
The state is not enough. This endpoint requires a bearer token, and the connection must belong
to that token’s company_id and user_id. A connection owned by someone else returns 404 —
identical to a nonexistent one, so ids can’t be probed across users. That is what stops a state
from being used to capture another user’s access token, and it is why the public callback only
stores the code instead of exchanging it.
A failed exchange leaves the flow in place so it can be retried; a failed tools/list validation
consumes it, since the token has already been issued.
Permission
agent_conversations resource with write verb.
Example
curl -X POST "$API/mcp-connections/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "Yk8fQ2Zsb3dTdGF0ZQ"}'
Common errors
| Status | Cause |
|---|---|
| 400 | state is malformed, unknown, or expired |
| 400 | the callback has not delivered the authorization code yet |
| 400 | the token exchange with the authorization server failed |
| 400 | the new token failed the tools/list validation against the MCP server |
| 404 | the connection does not exist, or belongs to another user |
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Body for POST /mcp-connections/complete. The state identifies the authorization flow to
finish; the authorization code itself is already held server-side and is never sent by the
client.
The one-time state issued by POST /mcp-connections and forwarded to the app by
GET /mcp-connections/callback. Consumed once the connection completes.
512"Yk8fQ2Zsb3dTdGF0ZQ"
Response
Connection completed. Returns the status view, normally with status: connected and the
access token's expires_at.
Status view of one connection. Deliberately credential-free — the registered client secret and the encrypted access/refresh tokens are never returned.
"b3f1c2a4-5d6e-47f8-9a0b-1c2d3e4f5a6b"
The external MCP server this connection targets.
"https://huggingface.co/mcp"
"huggingface"
The OAuth issuer discovered for the server.
"https://huggingface.co"
Lifecycle. pending after creation, and while waiting on the user's consent and the
completion call; connected once the token exchange succeeds and tools/list validates;
error for a failed connection.
pending, connected, error "connected"
"2026-07-12T17:00:00Z"
"2026-07-12T17:02:11Z"
When the current access token expires, if the server issued an expiry. The platform refreshes it automatically before use when a refresh token is available.
"2026-07-12T18:30:00Z"

