DEV Community

Cover image for How an MCP server calls your API on behalf of users: a production token strategy
Xiao Yijun for Logto

Posted on Originally published at blog.logto.io

How an MCP server calls your API on behalf of users: a production token strategy

This article is created by Logto, an open-source solution that helps developers implement secure auth in minutes instead of months.

In our previous article, we shared our overall experience of building the Logto remote MCP server. This article covers the architecture design and the OAuth flow in detail.

With the MCP server as the boundary, authentication for a remote MCP server has two legs: inbound and outbound.

  • Inbound: the MCP client (VS Code, Cursor, etc.) signs in through OAuth, gets an access token, and uses it to access your MCP server
  • Outbound: when handling tool calls, the MCP server requests your own business API on behalf of the user

The inbound leg is well defined in the MCP spec, with plenty of discussions and implementations in the community. We also wrote an implementation guide before. The outbound leg gets much less attention: the MCP server only holds a token for accessing the MCP server itself. How can it call your business API on behalf of the user?

This is the question we kept struggling with while building the Logto MCP server. Logto Cloud is a typical B2B multi-tenant product: a user can belong to multiple tenants, and permissions come from the user's role in each tenant. The AI not only needs to act as the user, it also needs to land on the right tenant.

This article follows our actual decision process:

  • Why the MCP server should be deployed independently, as a separate protected resource
  • Two outbound approaches we rejected (token passthrough and M2M token) and their problems
  • The final design, token exchange plus subject token, including how organization tokens handle multi-tenancy
  • A few principles to keep when you build something similar

The core problem: the dual role of an MCP server

From the OAuth perspective, a remote MCP server plays two roles at the same time:

mermaid-0

  • To the MCP client, it is a resource server: the client must bring a token to access it
  • To the business API, it is a client: it brings a token to access someone else

In other words, the MCP server plays a different role on each side, and each side uses a different token. The token the MCP client gets through OAuth has the MCP server as its audience, so the business API will reject it when validating the audience.

So the outbound leg is essentially a delegation problem: how does the MCP server call downstream APIs as the user, within the user's permissions, without holding the user's credentials?

Architecture decision: the MCP server as a standalone service

Building the MCP endpoint into the business API service, sharing the same process and the same auth stack, looks like the most direct option. After weighing it, we chose standalone deployment:

  • Risk isolation: when we made the decision, the official MCP SDK was not production ready, and the protocol itself was evolving fast (the transport switch from SSE to Streamable HTTP is one example). Logto is an IAM service, and the availability of the sign-in path is the bottom line. With standalone deployment, if the MCP server breaks, only the AI entry point goes down. The main service stays unaffected.
  • Independent iteration: the MCP ecosystem changes weekly, and client compatibility issues need hotfixes at any time, while the core service has a strict release process with regression tests. Separate deployments keep the two from slowing each other down.
  • Runtime freedom: a standalone service can pick the runtime that fits it best. The Logto MCP server runs on Cloudflare Workers: stateless, scales per request, and nearly zero ops. The embedded option does not give you this choice.

The MCP server is deployed on its own domain, mcp.logto.io, with no private coupling to the main service. If we want to open source it someday, nothing stands in the way.

The embedded option cannot avoid the token problem either: the MCP endpoint and the business API would share the same resource identifier, so the token the MCP client gets carries full API permissions. The question of "whose permissions does this call run with" moves from cross-service token exchange to in-process permission passing, and the problem itself remains. Standalone deployment forces us to design the permission boundary explicitly, which is what the rest of this article is about.

The MCP server must have its own resource identifier

The outbound discussion starts from a more basic question: what should be the audience of the token the MCP client gets?

Reusing the business API's resource identifier is the most direct option: Logto's Management API is already a standard OAuth protected resource, so the MCP client could request its token directly and the MCP server would forward it as is. Many early MCP server implementations did exactly this.

The cost: if the MCP token's audience is the business API, the permission boundary no longer exists.

  • Your carefully designed MCP tools become decoration: the token itself can call the full API without going through your tools
  • The blast radius of a leaked token grows from "a few controlled operations exposed by the MCP server" to "the entire Management API"
  • Independent auditing, rate limiting, and permission scoping for MCP scenarios have nothing to build on

So our first decision: the MCP server is a separate protected resource, with its own resource identifier (https://mcp.logto.io) and its own scope.

This decision makes the permission boundary clean, and it also makes the outbound question concrete: the token can only access the MCP server, so what does the MCP server use to call the API?

Now let's walk through the approaches.

Approach 1: token passthrough

The first idea came from a natural analogy.

Logto Console is a SPA. The way it calls the Management API is simple: the user signs in through OAuth in the browser, gets a token with the Management API as its audience, and the frontend calls the API with it directly.

So can the MCP server work as another kind of Console? Let the MCP client request the business API's token at sign-in, and the MCP server forwards it without any conversion:

mermaid-1

The appeal of this approach is that it is extremely simple: the MCP server only forwards tokens. But it has obvious problems:

First, it directly conflicts with the permission boundary decision above. Token passthrough requires the MCP client to hold the business API's token, which is exactly what we just rejected.

Second, the MCP server is no longer a real protected resource. The audience of the tokens it receives is not itself, so audience validation becomes meaningless and degrades into "verify the signature and issuer". This does not match how the MCP spec defines authorization (the MCP server should act as a resource server and declare itself through RFC 9728 Protected Resource Metadata). It essentially disguises a backend service as a SPA in the browser.

Third, MCP clients will not cooperate. A spec-compliant MCP client requests tokens following the Protected Resource Metadata, and the audience will be the MCP server. There is no standard way to make it request the business API's token, so this path does not work on the client side.

Approach 2: M2M token

If the user's token does not work, what about the MCP server's own identity?

Give the MCP server an M2M (machine-to-machine) application, get a token through client credentials, and call the business API with it. This is also the standard practice between internal services.

mermaid-2

Inbound auth also works fine now: the MCP client's token has the MCP server as its audience, the MCP server validates it normally, then does the work with its own M2M token.

The fatal flaw is that the M2M token's permissions have nothing to do with the user's permissions:

  • Permission overreach: the M2M token's permissions represent "what the MCP server can do", not "what this user can do". A user with a read-only role can delete applications through MCP, because the M2M token has that permission
  • Identity loss: the downstream API always sees the M2M application as the caller, and audit logs cannot be traced back to a specific person
  • Confused deputy: the MCP server becomes a high-privilege proxy, and anyone who can talk it into making a call borrows its permissions

M2M fits scenarios without user context, like scheduled jobs and system-to-system sync. An MCP server is different: every call is initiated by a specific user, so it should run with that user's identity and permissions.

The final design: token exchange + subject token

Put the lessons from the two approaches together and you get the requirements for the right design:

  1. The token held by the MCP client can only access the MCP server (lesson from approach 1)
  2. When calling downstream APIs, the MCP server must act as the user, within the user's permissions (lesson from approach 2)

This points to a standard mechanism: token exchange (RFC 8693). The MCP server takes a credential that represents the user and exchanges it at the auth server for a downstream API token. The user's identity and permissions are preserved through the exchange.

In Logto, this "credential that represents the user" comes from the user impersonation feature: the subject token. It is a short-lived credential the server requests for a specific user, meaning "the next token exchange runs as this user". The user does not need to create or configure anything, the whole flow is automated. The subject token is short-lived and single-use, and expires once used.

There are four roles in the architecture. The MCP server is deployed independently at mcp.logto.io:

mermaid-3

The full token flow behind one tool call:

mermaid-4

Step by step:

① Inbound validation. The MCP client calls a tool with the user token. The token's audience is the MCP server's own resource identifier, and its scope is mcp:all. The MCP server verifies the signature, issuer, audience, and scope, and gets the user's identity. Inbound stops here. This token never goes downstream.

② Service identity. The MCP server uses its own M2M credentials to get an access token with a dedicated scope, access:mcp:api. This scope has exactly one purpose: calling the dedicated endpoint in the next step.

③ Requesting the subject token, the key step of the whole chain. The MCP server calls POST /api/mcp/subject-tokens, an endpoint Cloud opens specifically for MCP, presenting two credentials at the same time:

  • Authorization header: the M2M token, proving "I am the official MCP server"
  • x-mcp-user-token header: the user's token, proving "this user has authorized me, and the authorization is still valid"

Cloud fully verifies the user token: signature, issuer, expiry, the audience must be the MCP server's resource identifier, and the scope must include mcp:all. After verification, the userId comes directly from the token's sub claim. The endpoint has no parameter for specifying a user.

This design prevents the M2M credential from being abused. If the endpoint accepted an arbitrary userId, anyone holding the M2M credential could impersonate any user. With this design, the MCP server can only exchange credentials for a user when that user's valid authorization is present.

The issued subject token is short-lived and single-use. The implementation never caches it and requests a fresh one for every use.

④ Exchanging for working tokens. With the subject token, run a standard token exchange to get two kinds of tokens as needed:

  • Cloud API token: for user-level operations as the user, like listing tenants and creating tenants
  • Org token: scoped to a target tenant (each tenant in Logto Cloud maps to an organization). The token is issued as the user, and its permissions are exactly the user's role in that tenant

⑤ Outbound call. Call the business API with the exchanged token, then return the result to the MCP client.

The multi-tenant context is also resolved at the exchange step: the tenant lives in the exchange layer, not the connection layer. list_tenants lists the options with the Cloud API token, the user picks one in the conversation, and the MCP server exchanges an org token for the chosen tenant. One endpoint serves all tenants, no per-tenant deployment needed, and a tenant created mid-conversation is available immediately.

Why this chain works

Check it against the failure points of the earlier approaches, and each one is covered:

  • The user token's blast radius is contained (lesson from approach 1): its audience is only the MCP server. Even if leaked, it can only call those controlled tools
  • The M2M token cannot be abused (lesson from approach 2): it no longer calls the business API directly and only proves the service identity. Issuing a subject token requires presenting the user's valid token at the same time, so the MCP server can only exchange credentials for users who have authorized it. The confused deputy problem is gone
  • No user credentials to store: subject tokens are requested on the spot and discarded after use. The MCP server stores nothing except its own M2M credentials
  • Revocation is connected: when the user revokes the MCP authorization, the user token becomes invalid, the x-mcp-user-token check fails, and the outbound chain stops right there
  • Permissions align with the user: exchanged tokens are issued as the user. A read-only user stays read-only through MCP, and privilege escalation fails at the auth server level. Audit logs also show the real user identity

Looking back, the M2M credential has the right job in the final design: it proves "who I am", while the ability to "act as the user" must be exchanged on the spot with the user's valid authorization.

Closing thoughts

Looking at the whole chain, the token strategy for a remote MCP server comes down to a few points:

  • Inbound and outbound are two separate legs of authentication, and the MCP token's audience must be the MCP server itself
  • Outbound is solved by token exchange: if your auth server supports exchanging the inbound token directly, standard token exchange is enough; if not (for example, Logto's token exchange takes a subject token as input), use an impersonation-style feature to start the exchange from the server side
  • Multi-tenancy does not change the mechanism. The organization context is a parameter at exchange time, and single-tenant products simply skip it
  • The AI acts on behalf of the user, so downstream token permissions must narrow down to that user

A simple test: suppose the token held by the MCP client leaks. All an attacker should be able to do is call those controlled tools on the MCP server, still within the user's permissions. If they can reach the full API directly, the permission boundary is broken.

The MCP ecosystem is still evolving fast. Inbound auth is well covered by the spec, while "how the MCP server calls downstream" is still up to each team. We hope our practice gives you a useful reference.

If you are building an MCP server for your own product, check out Logto's solution for AI scenarios: Auth for AI apps, agents, and MCP servers. To see this token chain in action, connect to the Logto MCP server and try it.

Technical references mentioned in this article:

Top comments (0)