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.
Reading the diagram
The trap in every two-box drawing is that it collapses two separate OAuth relationships, each carrying its own token, into one arrow.
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. DCR is optional in the spec, but interactive clients lean on it because it's how they get credentials without you provisioning each one by hand. 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:
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.
Code targets FastMCP 3.x (gofastmcp.com); current release on PyPI is 3.4.6 as of August 6, 2026. Re-check signatures against the live docs before you ship.
One spec note since this was drawn: the current MCP revision is 2026-07-28, which makes the resource parameter required in authorization and token requests (it was optional in 2025-06-18) and tightens iss handling. The broker pattern in the diagram is unchanged, but if you hand-rolled the endpoints instead of using OAuthProxy, check those two parameters.