How an MCP server calls your API on behalf of users: a production token strategy
Explains the outbound token strategy for MCP servers: why token passthrough and M2M fail, and how token exchange keeps permissions aligned with users.
Explains the outbound token strategy for MCP servers: why token passthrough and M2M fail, and how token exchange keeps permissions aligned with users.
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.
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:
From the OAuth perspective, a remote MCP server plays two roles at the same time:
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?
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:
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 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.
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.
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:
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.
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.
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:
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.
Put the lessons from the two approaches together and you get the requirements for the right design:
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:
The full token flow behind one tool call:
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:
⑤ 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.
Check it against the failure points of the earlier approaches, and each one is covered:
x-mcp-user-token check fails, and the outbound chain stops right thereLooking 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.
Looking at the whole chain, the token strategy for a remote MCP server comes down to a few points:
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: