Search "mcp oauth flow" and you get fragments. A sequence diagram that stops at the token grant. An Entra ID screenshot. Somebody's whiteboard photo with two boxes and an arrow. None of them show the part that actually trips people up: when your MCP server fronts GitHub or Google, there are two OAuth relationships running, and the client only ever sees one of them.

Here's the whole thing in one picture.

The OAuth split, drawnYour server is the broker. The client never holds the upstream credential.RELATIONSHIP ARELATIONSHIP BMCP CLIENTClaude · Cursor · VS CodeHOLDSFastMCP JWTthe only credentialit ever seesYOUR MCP SERVERFastMCP + OAuthProxyTHE BROKEREncryptedupstream tokenreferenced by JWT idUPSTREAMGitHub · Google · AzurePROVIDESOne fixed appno Dynamic ClientRegistrationTHE SPLIT THAT MATTERSThe JWT goes to the client. The GitHub token stays with the server.

Reading the diagram

The trap in every two-box drawing is that it collapses two separate OAuth relationships into one arrow. They're separate relationships carrying different tokens.

Relationship A is between the client and your server. Claude connects, gets a 401 pointing at a discovery URL, runs PKCE and a consent screen, and walks away holding a FastMCP JWT. That JWT is the only credential the client ever touches. As far as Claude is concerned, your server is the auth server.

Relationship B is between your server and the upstream provider. This is your one hand-registered GitHub or Google app, with a fixed client_id and secret you set up once in their console. The token that comes back from GitHub stays on your server, encrypted, referenced by an ID inside the JWT from Relationship A.

The reason this matters past being tidy: GitHub, Google, Azure, AWS, and Discord don't support Dynamic Client Registration. The MCP client expects to register itself on the fly. The upstream provider has no such endpoint. Point the client straight at GitHub and you get 401 loops and redirect-URI mismatches.

OAuthProxy is what sits in the gap, acting as the broker. It shows a DCR-compliant face to the client (Relationship A) while using your single fixed app credential upstream (Relationship B). One side effect: a token your client holds can't be turned around and replayed against GitHub directly, which is the security property you want.

And on every tool call

The connection is the hard part. Once it's up, every tool call traces the same loop, and the JWT-vs-upstream-token split holds through every hop:

The runtime pathWhat happens on a tool call. The client's JWT never touches the upstream.MCP CLIENTholds the JWTYOUR MCPSERVERverifies, decrypts, callsUPSTREAMGitHub / Google APItool call + JWT1upstream token, decrypted23API response4result, no credsTHE CLIENT NEVER SEES THE UPSTREAM CREDENTIAL

Four steps. The client only ever sees the lime arrows. The orange arrows happen entirely on your server. A leaked JWT can be revoked without touching GitHub, and a leaked GitHub token can't happen at the client layer at all, because the GitHub token never went there.

The wiring

For the common providers there's a subclass so you're not assembling endpoints by hand:

import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
 
auth = GitHubProvider(
    client_id=os.environ["GITHUB_CLIENT_ID"],
    client_secret=os.environ["GITHUB_CLIENT_SECRET"],
    base_url="https://your-server.com",
)
 
mcp = FastMCP(name="My Server", auth=auth)

GoogleProvider and AzureProvider are the same shape. All three are OAuthProxy underneath.

If your upstream provider does support DCR (WorkOS AuthKit, Descope), you don't need the proxy at all. RemoteAuthProvider is the whole answer and the client registers itself. The diagram collapses back to Relationship A only.

I wrote the full version of this, passive token verification versus active OAuth, the DCR cliff, the transport rule, and the production gotchas, in MCP server authentication with FastMCP, end to end. This page is the picture, since the picture is what the search results were missing.


Code targets FastMCP 3.x (gofastmcp.com). Re-check signatures against the live docs before you ship.