Skip to main content
Use oauthProxy when your identity provider gives you one fixed clientId and clientSecret and cannot register MCP clients itself. GitHub and Google work this way. With oauthProxy, your MCP server becomes its own OAuth authorization server for MCP clients, and signs users in to the upstream provider as one ordinary OAuth app. The upstream client secret and the user’s upstream tokens stay on your server. MCP clients only receive tokens that your server issues. Tools call the upstream API as the user with upstreamToken(ctx.auth). If your provider supports Dynamic Client Registration (DCR), use a built-in provider or a Custom Provider instead. Those keep your server a resource server only.

How the proxy works

The proxy sits between two OAuth relationships. Toward MCP clients, it is the authorization server: it identifies clients, shows a consent page, and issues its own access and refresh tokens. Toward the upstream provider, it is a confidential OAuth client with one registered callback URL.

The sign-in flow

  1. The client calls the MCP endpoint without a token.
  2. The server answers 401 with the URL of its protected resource metadata.
  3. The metadata names the server itself as the authorization server and lists its /mcp/oauth/* endpoints. The client identifies itself with a client ID metadata document URL, or registers at /mcp/oauth/register.
  4. The client opens /mcp/oauth/authorize in the browser with its PKCE challenge and state.
  5. The proxy checks the client, the redirect URI, the PKCE challenge, and the RFC 8707 resource.
  6. The proxy shows its consent page.
  7. The user approves.
  8. The proxy redirects the browser to the upstream provider with the proxy’s own client_id and its own PKCE challenge.
  9. The user signs in to the upstream provider and approves the OAuth app.
  10. The upstream provider redirects the browser to /mcp/oauth/callback with a code.
  11. The proxy checks that the callback belongs to the browser that approved consent, using a signed cookie.
  12. The proxy exchanges the code on the server with the client secret and its PKCE verifier, then reads the user, for example from Google’s userinfo endpoint.
  13. The proxy redirects the browser to the client’s redirect URI with a one-time code and the client’s original state.
  14. The client receives the code.
  15. The client exchanges the code at /mcp/oauth/token with its PKCE verifier.
  16. The proxy returns its own access token (mcp_at_…) and refresh token (mcp_rt_…).
  17. The client calls the MCP endpoint with the access token. The proxy looks up the grant and refreshes the upstream token if it expires soon.
The proxy runs its own PKCE round with the upstream provider, so an intercepted upstream authorization code is useless to anyone but the proxy.

Set up a Google sign-in

1

Configure the Google Auth Platform

In the Google Cloud console, open Google Auth Platform for your project and set up the app’s branding and audience. While the publishing status is Testing, add each Google account that signs in under Audience > Test users.
2

Create an OAuth client

Under Clients, create a client of type Web application. Add the proxy’s callback as an Authorized redirect URI: http://localhost:3000/mcp/oauth/callback for local development. Google never sees the MCP client’s own redirect URI, so this one URL serves every client. Copy the client ID and the client secret.
3

Set the environment

.env
4

Configure the server

src/index.ts
5

Connect a client

Run mcp-use dev and connect a client to http://localhost:3000/mcp:
The client opens the consent page, then Google. After sign-in, call whoami.
The complete Google example keeps sign-ins in Firestore with firestoreStorage and stores each user’s notes under their Google subject.

Register the callback URL

Register exactly one callback URL with the upstream provider:
With the default basePath of /mcp, that is https://mcp.example.com/mcp/oauth/callback in production and http://localhost:3000/mcp/oauth/callback locally. The proxy serves all of its endpoints under <basePath>/oauth/: The server publishes protected resource metadata that names itself as the authorization server, and RFC 8414 authorization server metadata that lists these endpoints. You do not configure either by hand.

Use the upstream token in tools

upstreamToken(ctx.auth) returns the upstream access token for the signed-in user. It takes only the auth object, so a helper that receives ctx.auth can call it too. It throws when the request is not signed in through oauthProxy, so a tool never calls the upstream API without the user’s token. This tool lists the user’s next Google Calendar events. Enable the Google Calendar API in the Google Cloud project, and request its scope from Google:
Then call the Calendar API with the user’s Google token:
Keep the upstream token on the server. Do not return it in tool results, views, or _meta. ctx.auth on a proxy server contains: subject is the provider’s stable user ID: the OpenID Connect sub for Google, or the numeric user ID for GitHub. It is never an email address or a login name, which can change. Use subject as the key for any data you store per user.

Map the user

Pass mapUser to shape ctx.auth.user, for example to match the id field that the other providers use:
mapUser runs on every request. It controls the user shape, not who can sign in: any user of the upstream provider can complete the sign-in. To restrict users, throw from your tools, or check ctx.auth.user in middleware.

Choose an upstream provider

Google

google() always requests openid and, by default, email and profile. Add Google API scopes to scopes as your tools need them. google() requests offline access, so Google returns a refresh token and the proxy refreshes Google tokens on the server. If Google returns no refresh token and none is stored for the user, the proxy sends the user through Google consent once more. While the app’s publishing status is Testing, Google expires refresh tokens after 7 days unless the app requests only the openid, email, and profile scopes. Users then sign in again. Publish the app to remove the limit.

GitHub

In GitHub Developer settings, choose OAuth Apps > New OAuth App, and set the callback URL to <public origin>/mcp/oauth/callback. Turn on Expire user access tokens so that GitHub issues refresh tokens; the proxy then refreshes the GitHub token on the server.
For GitHub Enterprise Server, set baseUrl to the web host, and apiUrl when the REST API lives elsewhere. The GitHub example calls the GitHub API as the user with Octokit and keeps sign-ins in Redis.

Any OAuth 2.0 provider

Use upstream() for a provider that has authorization and token endpoints. fetchUser reads the signed-in user with the upstream tokens and must return a stable subject.
id appears in storage keys and ctx.auth.payload.provider; name appears on the consent page. Endpoints must use HTTPS (HTTP is accepted on localhost). PKCE is on by default; set pkce: false only for a provider that rejects it. Set issuer when the token endpoint returns OpenID Connect ID tokens, and authorizeParams for extra query parameters such as prompt.

Choose storage

The proxy keeps clients, sign-in state, one-time codes, grants, and upstream tokens between requests, so it needs a store that every instance of the server shares. The proxy encrypts every value with your secret before the store sees it, and stores only SHA-256 hashes of tokens in keys.
mcp-use/oauth/storage has adapters for Upstash, Redis, Postgres, Firestore, and Cloudflare D1 or Durable Objects. See OAuth Proxy storage to pick one or write your own.

What each environment requires

oauthProxy throws when the server mounts without a required storage or secret. memoryStorage() is refused outside localhost. Without a secret, the proxy logs a warning and uses a random key, so sign-ins end when the process stops.

Set and rotate the secret

The proxy derives its encryption and cookie keys from secret, which defaults to the MCP_USE_OAUTH_SECRET environment variable. Use a random string of at least 32 characters:
To rotate, pass an array. The first secret encrypts and signs; the others still decrypt and verify:
Keep the previous secret until every record written with it has expired: 30 days for refresh tokens, 60 days for stored upstream tokens. Records that no secret can decrypt read as missing, so their users sign in again. The proxy logs a warning, once for each kind of record, with the key ID that wrote it.

Restrict which clients connect

By default, any MCP client can connect. A client identifies itself in one of two ways:
  • Client ID metadata document (CIMD). The client uses an HTTPS URL as its client_id, and the proxy fetches its metadata from that URL. The MCP specification prefers this. Claude and ChatGPT use it.
  • Dynamic client registration (DCR). The client registers at /mcp/oauth/register and gets a mcp_client_… ID. Registered clients are public clients: the proxy never issues a client secret.
Set clients to allow only the apps you list. Dynamic client registration turns off while clients is set, because a registered client names itself.
Removing a client from clients, or turning off dcr, ends that client’s sign-ins for good. The next request with one of its tokens deletes the grant, and adding the client back does not restore it. A briefly wrong allowlist signs those users out.
clients controls which apps connect, not which users sign in. dcr: true together with clients throws. The proxy fetches metadata documents only from hosts that resolve to public addresses. On Node.js and Bun it connects only to the addresses it checked. On Cloudflare Workers and Deno, fetch resolves the host again, so list the trusted metadata documents in clients to close the DNS-rebinding window there. Passing a custom fetch turns the host check off; your fetch must then enforce its own network policy. The proxy shows a consent page before it sends the user upstream. The page names your server by its domain, the app that asks, where the browser returns, the upstream provider, and the scopes. Consent defends against a malicious client that reuses an earlier upstream approval (a confused-deputy attack), so it is on by default. consent.page takes a React component that the proxy renders on the server with react-dom/server, so install react and react-dom. The component receives ConsentPageProps and must render a form that posts to form.action with two decision buttons. form.action already carries the CSRF token, so post to it unchanged and add no hidden fields:
src/consent.tsx
src/index.ts
The page is never hydrated, and its Content Security Policy allows inline styles only. Put CSS in a <style> element; scripts do not run.

Scopes and token lifetimes

The proxy’s own scopes are separate from the upstream scopes:
  • Upstream scopes (google({ scopes })) are what the proxy requests from the provider on every sign-in.
  • Proxy scopes (scopesSupported or requiredScopes) are what the proxy advertises and grants to MCP clients. Requested scopes that the proxy does not know are dropped, not rejected, because clients often send upstream scope names such as read:user. When no known scope is left, the grant gets every supported scope.
accessTokenTtl can be longer than the upstream token’s lifetime. On each MCP request, the proxy refreshes the upstream token when it expires within 60 seconds and the provider issued a refresh token. One instance refreshes under a storage lock while the others wait for its result. When the upstream token has expired and cannot be refreshed, the request fails with 401 and the client signs in again. Each refresh returns a new refresh token. The previous refresh token works once more within 60 seconds, so a client can retry after a dropped response. Reusing it after that ends the whole sign-in, because someone else holds a copy. Revoking either token at /mcp/oauth/revoke ends the whole sign-in: the grant, its access tokens, and its refresh tokens.

Deploy to production

1

Set the public URL

Set MCP_URL to the public origin, with no path, for example https://mcp.example.com.
2

Register the production callback

Add https://mcp.example.com/mcp/oauth/callback to the upstream OAuth app.
3

Configure a shared store and a secret

Pass a store from mcp-use/oauth/storage that every instance can reach, and set MCP_USE_OAUTH_SECRET on every instance. All instances must share the same secret.
4

Decide which clients connect

Leave clients unset to accept any client, or list the apps you support.

Migrate from v1 oauthProxy

v2 keeps the idea of v1 oauthProxy: one OAuth app at a provider without Dynamic Client Registration (DCR) signs in every MCP client. The token model is new. v1 passed the upstream provider’s tokens through to the client, and verified them on each request with your verifyToken. v2 issues its own tokens to the client and keeps the upstream tokens encrypted on the server. In v1, the client holds the upstream token: In v2, the client holds the proxy’s token, and the upstream token stays on the server: Because of this change, v2 needs two things v1 did not: a secret to encrypt what it stores, and a shared storage outside localhost. This v1 server:
v1 server.ts
becomes this v2 server:
v2 src/index.ts
1

Add the new callback URL to the upstream OAuth app

Register https://<your server>/mcp/oauth/callback next to the v1 /oauth/callback URL. Remove the v1 URL after the v2 deployment is live.
2

Create a secret and a shared store

Set MCP_USE_OAUTH_SECRET to the output of openssl rand -base64 32 on every instance, and pass a store from mcp-use/oauth/storage. See OAuth Proxy storage.
3

Move token verification into the upstream

Delete verifyToken. Use github() or google(), or move the user lookup from verifyToken into upstream({ fetchUser }), which must return a stable subject.
4

Replace upstream token reads in tools

Replace every ctx.auth.accessToken that calls the upstream API with upstreamToken(ctx.auth).
5

Redirect the v1 endpoint paths

v1 served /authorize and /token at the root of the server. v2 serves them under the MCP path, for example /mcp/oauth/authorize. Some clients saved the v1 URLs when they signed in and keep calling them. Redirect both paths to the v2 endpoints:
src/index.ts

What users see after the upgrade

Each user approves the consent page and signs in with the provider once more, in the same MCP client. Nobody needs to remove the server from a client or add it again:
  1. The client’s next request carries its v1 token, which is the provider’s token. v2 accepts only tokens it issued, so it answers 401.
  2. The client tries to refresh. v2 does not know the v1 refresh token, so the refresh fails and the client starts a new sign-in.
  3. The client still sends the client_id that v1 gave it, which is the provider’s clientId. v2 accepts it with the redirect URI of the request and shows the consent page. For these clients, ctx.auth.clientId is the provider’s clientId.
v2 accepts the v1 client_id only while dcr is on, which is the default when clients is not set. With clients set, a client from v1 shows Unknown app, and the user must remove the server from the client and add it again.
For the rest of the v1 to v2 upgrade, see Migrate to v2.

Troubleshooting

Next steps

OAuth Proxy storage

Choose a storage adapter or write your own.

User Context

Use the signed-in user in callbacks.

Mixed Authentication

Serve public and sign-in tools from one endpoint.

API reference

Every oauthProxy option, type, and default.