Guides

Let your MCP server call your API for a user

Use your AuthPI issuer to exchange a user's MCP access token for a delegated token that your own API can verify.

Last updated 2026-09-07

Your MCP server receives an access token intended for the MCP resource. To call a different API on that user’s behalf, exchange it at your AuthPI issuer for a new token intended for that API. The user remains the subject; your MCP backend is recorded as the service acting for them.

This guide uses your issuer, your MCP server, and your API. AuthPI is a multi-tenant identity provider: each issuer defines its own identity and authorization boundary. An organization belongs within an issuer; it is not another issuer. Hosting two issuers on AuthPI does not make their tokens or organization memberships interchangeable.

Looking to connect to AuthPI’s hosted MCP instead? See Use AuthPI MCP.

The participants

ParticipantRole in this example
Human userSigns in through your issuer and consents to documents:read.
MCP consumerThe application connecting to your MCP, using its own OAuth client. It can be a registered public client or an issuer-enabled CIMD client.
Your MCP backendReceives the user’s MCP token and authenticates the exchange using a separate, confidential service client. Only this backend holds that client’s secret.
Your APIAccepts the resulting delegated token for https://api.example.com and authorizes access to the selected organization’s documents.

The flow is: user consent → MCP-audience access token → authenticated exchange → API-audience delegated token → your API. The consumer’s OAuth client and the backend’s exchange client are different clients, registered or resolved within the same issuer.

Prerequisites

  • An AuthPI issuer and its owning account. Use that issuer throughout this flow.
  • Your MCP resource at https://mcp.example.com/mcp and target API at https://api.example.com. Replace these example resource identifiers consistently with your own exact URIs.
  • A human user with active membership in an active organization under your issuer, with membership authority for documents:read.
  • A configured MCP authorization flow. Follow the MCP resource profile with your resource URI and both openid and documents:read in the issuer’s permitted resource scopes. Registered consumer clients also need that resource in allowed_resources and documents:read in settings.scopes. CIMD consumers follow the issuer’s CIMD policy, including an organization policy that permits the selected membership.
  • An operator API key with issuers.clients:manage on the owning account, used only to provision the exchange client. This authority also permits creating the client.
  • curl and jq for the commands below. They call the HTTP endpoints directly.

The user must request and consent to documents:read in the original MCP authorization flow, alongside openid. Membership authority alone does not supply OAuth consent. The resulting access token must have the MCP resource as its only audience and include the selected organization in its membership claims. If it already has an org_id, the exchange must use that same organization. See the authorization flow and organization selection.

Register the backend’s exchange client

Save this complete client-creation body as exchange-client-request.json:

{
  "name": "Documents MCP exchange",
  "type": "internal",
  "confidential": true,
  "settings": {
    "protocol": "oidc",
    "scopes": ["documents:read"],
    "openid": {
      "application_type": "m2m",
      "response_types": [],
      "grant_types": ["urn:ietf:params:oauth:grant-type:token-exchange"],
      "redirect_uris": [],
      "token_exchange": {
        "subject_resources": [
          { "resource": "https://mcp.example.com/mcp", "scopes": ["documents:read"] }
        ],
        "target_resources": [
          { "resource": "https://api.example.com", "scopes": ["documents:read"] }
        ],
        "max_access_token_age": 300
      }
    }
  }
}

internal describes a client owned by you. It does not grant privileges: an external client follows the same exchange rules. The explicit token_exchange policy grants the capability. This dedicated client uses the exchange grant exclusively; it is not the browser client and does not use client_credentials.

Set AUTHPI_ACCOUNT_ID, AUTHPI_ISSUER_ID, AUTHPI_KEY_ID, and AUTHPI_KEY_SECRET to your provisioning credentials and identifiers, then create the client once:

umask 077
curl --fail-with-body --silent --show-error \
  --user "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  --header 'Content-Type: application/json' \
  --data-binary @exchange-client-request.json \
  "https://api.authpi.com/v1/accounts/$AUTHPI_ACCOUNT_ID/issuers/$AUTHPI_ISSUER_ID/clients" \
  > exchange-client.json

EXCHANGE_CLIENT_ID=$(jq -er '.data.id' exchange-client.json)
EXCHANGE_CLIENT_SECRET=$(jq -er '.data.secret' exchange-client.json)

Creation returns HTTP 201. The secret is returned only at creation; store it in your backend’s secret manager. The operator API key is for AuthPI configuration, while the exchange client secret authenticates runtime exchanges. Neither belongs in the MCP consumer or browser.

Exchange a user’s MCP token

In your MCP backend, set MCP_ACCESS_TOKEN to the verified access token from the incoming MCP request and ORG_ID to the organization the user selected. In a multi-issuer backend, select the issuer and exchange credentials from trusted application configuration, not from an unverified token claim.

TOKEN_RESPONSE=$(curl --fail-with-body --silent --show-error \
  --user "$EXCHANGE_CLIENT_ID:$EXCHANGE_CLIENT_SECRET" \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$MCP_ACCESS_TOKEN" \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
  --data-urlencode 'resource=https://api.example.com' \
  --data-urlencode 'scope=documents:read' \
  --data-urlencode "org=$ORG_ID" \
  "https://idp.authpi.com/$AUTHPI_ISSUER_ID/token")

API_ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -er '.access_token')

The response is a delegated access token, not another login or a refresh token. Each requested permission must pass the scope and membership checks. A disallowed scope rejects the entire exchange; the server does not silently return a smaller grant.

Validate and use the token in your API

Start with Validate tokens in your API: verify the signature, access-token type, exact trusted issuer, audience, and expiry before reading authorization claims. Configure the audience as your API’s resource URI, not the MCP URI.

For a route that requires this delegation, also enforce:

  • Exactly your API’s single audience, a human sub with dat.type: "identity", and a single actor object whose act.sub equals client_id and identifies a service your API permits.
  • org_id equals the organization requested by the API route, with exactly that organization in the token’s organizations array. Scope every data query to that organization under the trusted issuer.
  • The route’s required permission is in the top-level scope; the membership scopes match that delegated grant. Do not substitute broader roles or permissions from another token.

Record the human sub and service act.sub separately in your audit trail. The token claims reference describes their meanings.

For example, if your API exposes GET /organizations/{org_id}/documents with those checks, call it from the MCP backend:

curl --fail-with-body --silent --show-error \
  --header "Authorization: Bearer $API_ACCESS_TOKEN" \
  "https://api.example.com/organizations/$ORG_ID/documents"

This is an example route you implement, not an AuthPI endpoint. Verify that it returns only the selected organization’s documents and records both identities. Then confirm the same route rejects the original MCP token, an access token from another issuer, and a request for another organization. A read-only delegated token must not authorize writes.

Handle failures and lifecycle changes

Use the error reference to distinguish bad client authentication, disallowed exchange policy, rejected subject or membership, and missing scope authority. Changing the service policy cannot add a permission missing from the user’s original consent; obtain a new consented MCP grant when that permission is needed.

User and membership status are checked when exchanging. Already-issued delegated tokens remain bounded by their expiry; a later change is not a promise of immediate invalidation at an API using local JWT validation. Follow the lifetime contract, and obtain a new delegated token through a fresh exchange when needed. Never forward the original MCP token to the target API or return the backend’s exchange secret to the consumer.

Next steps