MCP Protocol Flows
How MCP clients discover, authenticate with, and interact with the gateway. This covers the protocol-level flows that any MCP client (Claude Code, custom agents, etc.) follows.
Discovery and authorization
MCP clients follow the standard OAuth 2.0 authorization code flow with PKCE. The gateway exposes discovery endpoints so clients can locate the AS automatically.
Step-by-step
1. Initial call — no token
The client sends a request to POST /mcp without a valid Authorization header. The gateway returns HTTP 401 with a WWW-Authenticate header:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://gateway.policyarc.com/.well-known/oauth-protected-resource", scope="drive:read drive:write jira:read"
2. Discover protected resource metadata (RFC 9728)
The client fetches GET /.well-known/oauth-protected-resource:
{
"resource": "https://gateway.policyarc.com",
"authorization_servers": ["https://as.policyarc.com"],
"scopes_supported": ["drive:read", "drive:write", "jira:read"],
"bearer_methods_supported": ["header"]
}
3. Discover AS metadata (RFC 8414)
The client fetches GET /.well-known/oauth-authorization-server. The gateway proxies this to the AS and returns the metadata document unchanged. This proxy exists because the MCP spec requires clients to discover AS metadata at the MCP server's own base URL.
4. Dynamic Client Registration (optional)
If the AS supports DCR (RFC 7591), the client can register itself using the registration_endpoint from the AS metadata.
5. Authorization code flow with PKCE
The client redirects the user to the AS authorization endpoint. The resource parameter must be the gateway's resource indicator so the AS issues an audience-restricted token:
GET /authorize?
response_type=code&
client_id=<client_id>&
redirect_uri=<redirect_uri>&
scope=drive:read+jira:read&
resource=urn:mcp:gateway&
code_challenge=<pkce_challenge>&
code_challenge_method=S256
6. Token exchange
The client exchanges the authorization code for an access token:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=<code>&
redirect_uri=<redirect_uri>&
client_id=<client_id>&
code_verifier=<pkce_verifier>&
resource=urn:mcp:gateway
7. Call the gateway
POST /mcp
Authorization: Bearer <access_token>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"drive_list_files","arguments":{}}}
Step-up authorization
When a client receives a 403 with error="insufficient_scope", it should re-initiate the authorization flow requesting the additional scope:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
resource_metadata="https://gateway.policyarc.com/.well-known/oauth-protected-resource",
scope="drive:write",
error_description="Scope drive:write required for drive_write_file"
The client then redirects the user to re-authorize with scope=drive:write resource=urn:mcp:gateway. Only the incremental scope is needed.
Connecting an additional identity provider
A separate step-up fires when a tool is backed by a connector that forwards the user's own token to an upstream (per-user modes such as idp_passthrough and entra_refresh) and the user hasn't connected their account at that provider yet — or their connection has expired and can't be refreshed. Instead of failing, the gateway returns a one-time link the user opens to connect that provider.
On the REST surface (/gateway/{resource}/**) this is a 401 with a JSON body:
{
"error": "idp_link_required",
"idp": "gitlab",
"link_url": "https://as.policyarc.com/gateway/link?ls=..."
}
On the MCP surface (POST /mcp), delivery is gated on the client's capabilities:
- Clients that declared
elicitation.url(MCP URL elicitation, SEP-1036) receive a JSON-RPC-32042error whosedata.elicitations[0]carries{mode:"url", elicitationId, url, message}, so the client can prompt the user and retry automatically. - All other clients receive an ordinary
tools/callresult withisError:true, the connect link in the text, and an_meta.idp_link_required{idp, link_url}hint. The client relays this to the model, so the link still reaches the user.
The link_url is always on the AS issuer origin. Opening it hits GET /gateway/link?ls=<signed-state>, which requires the caller's PolicyArc browser session to match the identity the link was minted for before redirecting into the target provider's sign-in. If the browser has no session, the user is re-authenticated on demand against their sign-in provider and returned to the link; a session belonging to a different identity is refused (403). Once the user completes it, the token is stored against the same identity and the original tool call succeeds on retry.
A parallel step-up covers connectors whose upstream doesn't speak OAuth and instead takes a user-supplied credential (an API token or PAT). There the deny carries error="user_secret_required" and the link points at GET/POST /gateway/connect, where the user enters their credential on a PolicyArc-hosted form (the credential never transits the MCP client). The same session-match and on-demand re-auth rules apply. When a stored credential is later rejected by the upstream with a 401, the same collect step-up fires so the user can re-enter it; a 403 is relayed unchanged.
For the admin-facing view of both — what to configure and what users experience — see Connecting multiple accounts.
Resource indicators
The gateway's resource indicator is configured via resource_indicator in gateway.yaml (default: urn:mcp:gateway).
Resource indicators (RFC 8707) allow the AS to issue audience-restricted tokens. When the gateway calls /introspect, it passes the resource indicator as context:
{"resource": "urn:mcp:gateway", "scopes": ["drive:read"]}
This context is passed to OPA at evaluation time, allowing policy rules to enforce both resource binding and scope requirements.
Methods that skip introspection
These MCP methods bypass token validation:
| Method | Behavior |
|---|---|
initialize | Returns protocol version, capabilities |
notifications/initialized | No-op |
tools/list | Lists all configured tools |
resources/list | Lists fixed-URI resources |
resources/templates/list | Lists URI-template resources |
A Bearer token is still required in the Authorization header (401 if absent), but its validity is not checked for these methods.
WWW-Authenticate header behavior
The gateway constructs WWW-Authenticate headers per RFC 6750:
| Status | When | Header includes |
|---|---|---|
| 401 | No Authorization: Bearer header | resource_metadata URL + all configured scopes |
| 403 | Token valid but missing required scope | error="insufficient_scope" + the specific scope needed |