Agent + brokered MCP
At a glance. The MCP wraps an upstream OAuth provider (Google, GitHub, Linear). AuthPlane brokers tokens from the upstream IdP per request. The agent never sees the upstream credentials; the upstream API never sees AuthPlane. Per-user audit. Three-bound consent model.
Topology
flowchart TB
User["User"]
AS["AuthPlane"]
IdP["Upstream IdP<br/>(Google)"]
Agent["Agent"]
API["Upstream API<br/>(Google Cal)"]
Broker["Broker MCP<br/>slug:<br/>google-cal"]
User <-->|consent + connect| AS
AS <-->|OAuth refresh/vend| IdP
AS --> Agent
IdP --> API
Agent -->|"upstream bearer<br/>(vended by AS)"| API
Broker MCP — a Resource with backend_kind: broker, referencing a broker_provider row (the upstream OAuth app). Upstream IdP — signs the bearer the upstream API accepts. AuthPlane — orchestrates two consents (user→agent at AS, user→upstream at IdP) and vends a fresh upstream bearer per request.
Flow — first time (upstream connect needed)
sequenceDiagram
participant Agent
participant AS as AuthPlane
participant User
participant IdP as Upstream IdP
participant MCP as Broker MCP
participant API as Upstream API
Agent->>AS: /oauth/authorize?resource=google-cal&scope=…
AS-->>User: consent screen (Agent × google-cal)
User->>AS: approve
AS-->>Agent: 302 + code
Agent->>AS: /oauth/token grant_type=authorization_code
AS->>AS: AS notes: no broker_grant for (user, google) yet
AS-->>Agent: 400 consent_required, consent_url=/connect/google
Agent->>User: surface the connect URL
User->>AS: GET /connect/google?return_url=…
AS->>IdP: redirect to upstream OAuth authorize
User->>IdP: consent
IdP->>AS: callback with code → upstream tokens
AS->>AS: write broker_grants row (encrypted refresh)
AS->>User: 302 to return_url
User->>Agent: retry
Agent->>AS: /oauth/token (retry, now succeeds)
AS->>IdP: refresh/vend with stored upstream credential
AS-->>Agent: upstream bearer (narrowed scopes)
Agent->>MCP: call with upstream bearer
MCP->>API: forward upstream bearer
Flow — subsequent calls
sequenceDiagram
participant Agent
participant AS as AuthPlane
participant IdP as Upstream IdP
participant MCP as Broker MCP
participant API as Upstream API
Agent->>AS: /oauth/token (token-exchange or refresh)
AS->>IdP: vend per-request (broker_grants hit)
AS-->>Agent: fresh upstream bearer
Agent->>MCP: call with upstream bearer
MCP->>API: forward
The upstream bearer is never cached in AuthPlane — every /oauth/token hits the upstream IdP for a fresh vend.
When to use
- MCP is a thin proxy in front of a third-party OAuth-protected API.
- User’s upstream credentials should be managed centrally by AuthPlane (not scattered across MCPs).
- Upstream API expects its own bearer on the wire.
Don’t use when:
- MCP has its own scopes and issues its own tokens → use single-mcp.
- You want a gateway to sit in front → mcp-gateway-broker.
- Multiple agents share one broker MCP through a gateway → mcp-gateway-broker + fronting-link.
How to configure
Full walkthrough in Guides: Wire up the Token Vault. Summary:
YAML seed:
broker_providers:
- slug: google
display_name: Google
protocol: oauth
config_data:
client_id: "your-google-oauth-client-id"
client_secret_ref: CONNECTOR_GOOGLE_SECRET
authorize_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
extra_auth_params: { access_type: offline, prompt: consent } # both needed for Google to return a refresh_token
resources:
- slug: google-cal
display_name: Google Calendar
backend_kind: broker
broker_provider_slug: google
scopes:
- name: calendar:read
description: Read Google Calendar events
upstream: https://www.googleapis.com/auth/calendar.readonly
Data encryption (data_encryption.driver: aes_master or vault_transit_encrypt) is required when broker_providers are configured — boot fails otherwise.
How AuthPlane handles it
- Connect flow — user browses to
/connect/google, AuthPlane runs the upstream OAuth handshake, callback writes an encryptedbroker_grantsrow. - Exchange dispatch —
BrokerIssuerruns the three-bound consent gates (see below), then dispatches to thebrokerprotoadapter matching the provider’s protocol (oauth,api_key,service_account). - Refresh — automatic when the upstream access token has expired. Concurrent vends are serialized via optimistic locking (
423 Lockedon race — SDKs retry).
Three-bound consent — every vend runs through:
requested_scopes ⊆ consent_grants.scopes (per-agent attestation)
⊆ broker_grants.scopes_granted (per-provider grant)
Failure returns consent_required with the right consent_url (either AS-side re-consent or /connect/{provider}). SDKs auto-translate to MCP -32042 elicitation.
Verify
# Confirm the connection was recorded
authserver admin grant list-user-grants --user user-42
# → broker_grants (1): user-42, google, scopes_granted=[...]
# Confirm token-exchange works
curl -X POST http://localhost:9000/oauth/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=$USER_TOKEN" \
-d "resource=google-cal" \
-d "scope=https://www.googleapis.com/auth/calendar.readonly" \
-d "client_id=$MCP_CLIENT_ID" \
-d "client_secret=$MCP_CLIENT_SECRET"
# → { "access_token": "ya29...", "token_type": "Bearer", "expires_in": 3599 }
See also
- Guides: Wire up the Token Vault — full end-to-end
- Guides: Upstream connections — per-protocol broker provider setup
- Concepts: Token Vault — mental model
- Topologies: MCP gateway → broker — fronting variant
- Reference: Errors → Broker/Vault