On this page
MCP authentication works two ways: remote MCP servers sign you in with OAuth 2.1, and local (stdio) servers read a key from the environment. If your client keeps asking you to sign in again, the server usually issues no refresh token, so every expired access token means a fresh sign-in. Prefer servers with scoped OAuth over all-access keys.
Sign-in trouble is a steady theme on Reddit. Recent threads include r/laravel's "PSA: Lock down your MCP OAuth registrations" (20+ comments), r/mcp's "Why OAuth isn't enough", a GitLab MCP sign-in failing in Visual Studio 2026 with "Client ID is not available", and "ClickUp MCP keeps disconnecting" on r/hermesagent.
How MCP OAuth works: the authorization flow step by step
The rules are in the Authorization section of the current MCP specification (2026-07-28), with the details split into Authorization Server Discovery, Client Registration and Security Considerations. The short version, quoting the spec where it matters:
It's optional, and only for HTTP. "Authorization is OPTIONAL for MCP
implementations." Servers using stdio "SHOULD NOT follow this
specification, and instead retrieve credentials from the environment." That is
why local servers ask for an API key in env, and remote ones open a browser.
The MCP server is an OAuth resource server. "A protected MCP server acts as an OAuth 2.1 resource server", and your client acts as the OAuth client. The sign-in page belongs to an authorization server, which can be the same service or a separate one.
Discovery starts with a 401. "MCP servers MUST implement OAuth 2.0
Protected Resource Metadata (RFC9728)." When you connect without a token, the
server answers 401 Unauthorized, usually with a WWW-Authenticate header
pointing to a metadata document. If the header is missing, the client tries
/.well-known/oauth-protected-resource instead. That document names the
authorization server.
The client needs a client ID. For clients that support every option, the spec's priority order is: a pre-registered client ID, then a Client ID Metadata Document (an HTTPS URL that describes the client), then Dynamic Client Registration (DCR) "as a fallback". If none works, the client should prompt you to enter the details.
PKCE is required. "MCP clients MUST implement PKCE", using S256, and
must refuse to continue if the authorization server's metadata doesn't list
code_challenge_methods_supported.
Tokens are tied to one server. Clients must send a resource parameter
naming the MCP server, and "MCP servers MUST validate that access tokens
were issued specifically for them as the intended audience." A server "MUST
NOT pass through the token it received from the MCP client" to other APIs.
This stops one server's token being replayed against another.
Every request carries the token. The client sends
Authorization: Bearer <token> on every HTTP request, never in the URL. An
invalid or expired token gets a 401.
Scopes can grow when needed. If a token lacks a permission, the server
should answer 403 with error="insufficient_scope" and the scopes it needs,
and the client should run a "step-up" sign-in for those scopes.
What changed in the 2026-07-28 spec
The previous version of this guide quoted the 2025-11-25 spec. The core flow above is unchanged. These are the differences that matter for sign-in:
| Change | What it means |
|---|---|
| Dynamic Client Registration is deprecated | "New implementations should use Client ID Metadata Documents instead." DCR stays for backwards compatibility. Expect fewer "register your app" errors as clients and providers move to metadata documents |
| New refresh-token rules | Clients "MUST NOT assume refresh tokens will be issued" and MAY add offline_access when the authorization server lists it. MCP servers SHOULD NOT put offline_access in their own scope lists |
| Issuer checks (RFC 9207) | Authorization servers SHOULD return an iss value with the code, and clients MUST check it before redeeming the code. Metadata whose issuer doesn't match the URL it came from MUST be rejected. This blocks "mix-up" attacks |
| Credentials bound to one authorization server | Clients key stored client credentials by issuer and MUST re-register if the server moves to a different authorization server |
application_type in DCR |
Desktop and CLI clients SHOULD register as "native", which avoids redirect-URI rejections from OpenID Connect providers |
| Step-up keeps old scopes | When re-authorizing, the client asks for the union of its earlier scopes and the new ones. Servers SHOULD name every scope an operation needs in one challenge |
| No sessions | MCP is now stateless: the initialize handshake and the Mcp-Session-Id header are gone. Each request stands alone, and the token on it is what tells the server who is calling |
Full list: the spec's changelog.
MCP server authentication: how to add OAuth to your own server
If you're building a remote MCP server, this is the minimum the spec asks of the server side. A local stdio server doesn't need any of it: read a key from the environment, as in keeping API keys out of your MCP config.
-
Use an existing authorization server. Your company identity provider, or the API provider whose data the server reads, issues the tokens. Writing your own login and token service is rarely worth the risk.
-
Publish Protected Resource Metadata at
/.well-known/oauth-protected-resource(or at that path plus your MCP endpoint path), withauthorization_serverslisting at least one authorization server. -
Answer unauthenticated requests with
401and aWWW-Authenticateheader carryingresource_metadataand, ideally, thescopeneeded:HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read" -
Validate every token on every request: signature or introspection, expiry, and an audience that names your server. "MCP servers MUST NOT accept or transit any other tokens."
-
Never forward the client's token. If your server calls another API, it gets its own token for that API.
-
Keep scopes small. Ask for read scopes up front and return
403 insufficient_scopewith the extra scope when a tool needs write access. Leaveoffline_accessout of your scope lists; refresh tokens are the authorization server's call. -
Check the authorization server's settings: PKCE with
S256advertised, exact redirect-URI matching, Client ID Metadata Document support (DCR only if you need older clients), and anissvalue in authorization responses. -
If you keep DCR open, rate-limit it. That's the point of the r/laravel PSA: an open registration endpoint with no limit lets anyone create client registrations.
-
Proxying a third-party API with one static client ID? The spec requires per-client user consent before forwarding to the third party's authorization server (the "confused deputy" rule).
Test it with the MCP Inspector:
connect once with no credentials and you should get "auth required" (CLI exit
code 3), then run the full OAuth flow and watch the 401 and the
Authorization header in the Network tab. If many servers need the same
company sign-in, an MCP gateway
can handle it centrally. The rest of the server-side checklist is in
how to create an MCP server, securely.
Need OAuth added to your MCP server, or a hardened server built from scratch? Tell us what it connects to.
Why your client keeps asking you to sign in
| Cause | What you see | Fix |
|---|---|---|
| No refresh token issued | A new sign-in every time the access token expires | Ask for offline_access if the provider lists it (Claude Code adds it automatically; in Cursor add it to auth.scopes) |
| Refresh token rejected or rotated away | A "re-authenticate" notice after working for a while | Sign in again from the client's MCP menu |
| Client can't register (no DCR, no metadata document support) | Errors like "does not support dynamic client registration" or "Client ID is not available" | Register an OAuth app with the provider and give the client a static client ID |
| Redirect URI mismatch | Browser shows an error after you approve | Use the callback URL the provider has registered for your client |
A static Authorization header is configured |
Connection fails instead of opening a browser | Remove the header to use OAuth, or fix the token |
| Sign-in per client and per machine | Signed in on your laptop, not in CI or a cloud agent | Sign in once in each environment |
Claude Code
From Claude Code's MCP docs (more commands in our Claude Code MCP setup guide):
- "Authentication tokens are stored securely and refreshed automatically."
When a stored token gets a
401, Claude Code refreshes it, reconnects and retries once. If the refresh token itself is rejected, you get a notice; open/mcpand choose Re-authenticate. - If the authorization server lists
offline_access, Claude Code adds it so the token can be refreshed without a new browser sign-in. - Sign in from your shell instead of a session. Over SSH, add
--no-browserto get a URL to open on your own machine:
claude mcp login sentry
- Headless runs (
claude -p, the Agent SDK) can't run the browser flow. Sign in from an interactive session or withclaude mcp loginfirst. - Claude Code discovers servers that use a Client ID Metadata Document on its own. If the server supports neither that nor DCR, register an app with the provider, then add the server with your client ID (the secret is prompted for and stored in the system keychain or a credentials file, not in your config):
claude mcp add --transport http \
--client-id your-client-id --client-secret --callback-port 8080 \
my-server https://mcp.example.com/mcp
Use http://localhost:PORT/callback as the redirect URI in the provider's form,
with the same port.
- To limit what you grant, pin scopes with
oauth.scopesin the server's config. If a tool later fails with "needs additional permissions", add the named scope there and authenticate again.
Cursor
From Cursor's MCP docs:
- Cursor uses the authorization code flow with PKCE and "renews an expired access token with the refresh token the server issued." If the server returns an expiring token with no refresh token, you sign in again when it expires.
- If your provider only issues refresh tokens for a scope such as
offline_access, add that scope toauth.scopesinmcp.json. - For servers without DCR, set a static
CLIENT_ID(andCLIENT_SECRETif the provider uses confidential clients) in the server'sauthblock, using environment variables rather than pasted values. - Cursor's desktop callback is
http://localhost:8787/callback. - Cloud Agents keep their own sign-ins, separate from your machine's. Signing out locally doesn't sign them out.
For other clients, check their docs for refresh-token support before blaming the server.
OAuth vs API key vs personal access token
Authentication is the most heavily weighted of the four checks in our trust grade. What matters is less the method and more how much a leaked credential could do.
| Method | How it works | Typical risk | Example from the directory |
|---|---|---|---|
| OAuth with real scopes or grants | Browser sign-in, you approve specific access | Lowest: a token covers only what you granted | Stripe (A): you grant each account or sandbox. Slack (A): per-tool scopes plus admin approval |
| OAuth with all-or-nothing access | Browser sign-in, but one grant covers everything you can do | Medium: no key on disk, but the agent can do anything you can | Notion (B): full Notion permissions, no scopes |
| Personal access token (PAT) | You create a token and paste it into config | Medium to high: lives on disk, often broad, often long-lived | GitHub (A): hosted use in Claude Code and Cursor means pasting a PAT; its OAuth asks for every scope by default |
| API key | A static key in env or a header |
Highest if it acts for the whole account | Most local servers. See keeping API keys out of your MCP config |
| None | No sign-in | Fine for public read-only data; risky for anything else | Microsoft Learn (public docs) |
Safety note: watch for "full user permissions"
OAuth feels safer because there is no key to leak, but many hosted servers grant the agent everything your account can do. Examples from our listings:
- Notion: OAuth acts with your full Notion permissions; there are no scopes or read-only mode for OAuth connections.
- Figma: one scope,
mcp:connect, covers read and write tools alike. - Asana: OAuth without scopes.
- Dropbox: full Dropbox access unless you register your own app.
What to do about it:
- Use a separate account or workspace for agent work where the vendor allows it.
- Prefer read-only options when they exist, for example the read-only endpoints of GitHub and Buildkite, or the read-only grant on Cloudflare.
- Keep tool approval on for anything that writes.
- Revoke when you're done. In Claude Code,
claude mcp logout <name>revokes tokens where the provider supports it. Removing the server alone deletes the local tokens but doesn't revoke them.
Before you connect a new server at all, run the 5-minute security checklist.
Frequently asked questions
What is MCP OAuth?
It's the sign-in flow the MCP spec defines for remote servers: OAuth 2.1 with PKCE, where the MCP server is the resource server and your AI app is the OAuth client. The server points the client to its authorization server, you approve access in the browser, and the client sends the resulting token on every request.
What is the difference between MCP authentication and MCP authorization?
Authentication is signing in: proving who you are. Authorization is what that sign-in grants. The MCP spec covers both in its "Authorization" section: you sign in on the authorization server's page, and the client gets an access token that allows specific access (scopes) to one MCP server. So when people say "MCP authentication", the spec's answer is the OAuth authorization flow above.
Does MCP require authentication?
No. The spec makes authorization optional, and says local stdio servers should take credentials from the environment instead. Remote servers that protect user data are expected to use the OAuth flow the spec defines. A remote server that lists its tools with no credentials at all can be used by anyone who finds the URL.
How do I add authentication to my MCP server?
Publish Protected Resource Metadata, return 401 with a WWW-Authenticate
header to unauthenticated requests, let an existing authorization server issue
tokens, and check each token's audience on every request. The
MCP server authentication
section above has the full list.
Why does my MCP server keep asking me to log in?
Most often the server issues an expiring access token without a refresh token,
so the client can't renew it silently. Adding offline_access can fix it if
the provider lists that scope (Cursor: auth.scopes; Claude Code adds it
itself). Otherwise, the provider has to change.
Is Dynamic Client Registration deprecated in MCP?
Yes, as of the 2026-07-28 spec. Client ID Metadata Documents replace it: the
client's ID is an HTTPS URL that describes it, so nothing has to be registered
in advance. DCR still works with providers that haven't moved. If a provider
supports neither, register an app yourself and give the client its client ID
(--client-id in Claude Code, auth.CLIENT_ID in Cursor).
Can I use an API key with an MCP server instead of OAuth?
Yes, but outside the spec's OAuth flow. Local stdio servers are meant to read
credentials such as API keys from the environment. Many remote servers also
accept a key or token in a static Authorization header; in Claude Code, a
configured header replaces the browser sign-in. Keep keys out of the config
file itself, as shown in
keeping API keys out of your MCP config.
Is a personal access token safer than OAuth?
Usually not. A PAT sits in a file, rarely expires, and is often created with broad permissions. A scoped OAuth grant that the client refreshes and stores securely is the better default. If you must use a PAT, give it the fewest permissions and an expiry date.
Why is OAuth "not enough" for agents?
The r/mcp thread makes a fair point: a token says who granted access, not which agent is acting. Logs show your account either way. Separate accounts for agents, narrow grants and tool approval close most of that gap today.
More: Is this MCP server safe to install? · MCP gateway · MCP security best practices · Browse the directory