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 encrypted broker_grants row.
  • Exchange dispatchBrokerIssuer runs the three-bound consent gates (see below), then dispatches to the brokerproto adapter 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 Locked on 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