MCP OAuth guide
MCP OAuth: How MCP Server Authentication Works, Step by Step
Remote MCP servers sign users in with OAuth 2.1. This guide follows one real connection request by request, using MCPBackend's production endpoints, then covers client setup, API keys versus OAuth, and the errors you are most likely to hit.
Updated October 27, 2026
The short answer
The MCP authorization specification treats a remote MCP server as an OAuth 2.1 resource server. The client (Claude, Cursor, Claude Code and others) gets an access token from an authorization server and sends it as Authorization: Bearer …on every request. Everything else in MCP auth is about letting a client do that with nothing but the server's URL:
- Discovery. A 401 response points to protected resource metadata (RFC 9728), which names the authorization server, whose own metadata (RFC 8414) lists its endpoints.
- Registration. The client registers itself through dynamic client registration (RFC 7591), so nobody has to create an OAuth app by hand.
- Sign-in. The user approves access in a browser with the authorization code grant and PKCE. Passwords and codes never pass through the AI tool.
- Audience. The token is issued for one resource (RFC 8707), so a token for one MCP server cannot be replayed against another.
Local servers that run over stdio do not use any of this. They inherit your machine's user and normally read secrets from environment variables. OAuth matters once a server is reached by URL and acts on someone's account.
The flow, request by request
These are the requests a client makes the first time it connects to MCPBackend's server at https://mcp.mcpbackend.com/mcp. The responses are copied from production; you can repeat the first three with curl.
1. The first call fails with 401 and a pointer
POST https://mcp.mcpbackend.com/mcp (no Authorization header)
HTTP/2 401
www-authenticate: Bearer realm="OAuth",
resource_metadata="https://mcp.mcpbackend.com/.well-known/oauth-protected-resource/mcp",
error="invalid_token", error_description="Missing or invalid access token"2. Protected resource metadata names the authorization server
GET https://mcp.mcpbackend.com/.well-known/oauth-protected-resource/mcp
{
"resource": "https://mcp.mcpbackend.com/mcp",
"authorization_servers": ["https://mcp.mcpbackend.com"],
"scopes_supported": ["mcp:tools"],
"bearer_methods_supported": ["header"]
}3. Authorization server metadata lists the endpoints
GET https://mcp.mcpbackend.com/.well-known/oauth-authorization-server
{
"issuer": "https://mcp.mcpbackend.com",
"authorization_endpoint": "https://mcp.mcpbackend.com/authorize",
"token_endpoint": "https://mcp.mcpbackend.com/token",
"registration_endpoint": "https://mcp.mcpbackend.com/register",
"scopes_supported": ["mcp:tools"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"]
}Two details matter here. Only S256 PKCE is accepted (plain PKCE and the implicit flow are switched off), and public clients can use token_endpoint_auth_method: "none", which is what desktop and CLI clients normally do.
4. The client registers itself
POST https://mcp.mcpbackend.com/register
{
"client_name": "Example MCP client",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
→ 201 { "client_id": "…", ... }The client keeps that client_id. On MCPBackend a registration lasts 90 days; after that the client registers again.
5. You approve access in the browser
GET https://mcp.mcpbackend.com/authorize
?response_type=code
&client_id=…
&redirect_uri=http://127.0.0.1:33418/callback
&scope=mcp:tools
&resource=https://mcp.mcpbackend.com/mcp
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
&state=<random>The server validates the request and redirects you to the consent page in the MCPBackend dashboard. If you are signed in you see the client's name, your account email and the requested access, and you click Connect or Deny. If you are not signed in, you sign in with Google or a four-digit code sent to your email and come straight back to the same consent screen. The pending request is single-use and expires after 10 minutes. Approving sends the browser to the client's redirect_uri with a one-time code; denying sends error=access_denied.
6. The client exchanges the code for tokens
POST https://mcp.mcpbackend.com/token
grant_type=authorization_code&code=…&client_id=…
&redirect_uri=http://127.0.0.1:33418/callback&code_verifier=<original verifier>
→ { "access_token": "…", "token_type": "bearer", "expires_in": 3600,
"refresh_token": "…", "scope": "mcp:tools" }7. Every MCP request carries the token
POST https://mcp.mcpbackend.com/mcp
Authorization: Bearer <access_token>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/list"}The access token lasts one hour. Before it runs out the client calls /token with grant_type=refresh_token; refresh tokens last 28 days, so you only see the consent screen again if a client sits unused for about a month. The single mcp:tools scope lets the agent use the MCP tools as your account, with the same plan limits and permissions you have in the dashboard.
What an MCP client needs to support
When a server's sign-in fails in one client but works in another, it is usually one of these capabilities.
| Capability | Why it matters | MCPBackend expects |
|---|---|---|
| Streamable HTTP transport | Remote servers are reached by URL, not started as a process | POST to /mcp |
| Resource and authorization server discovery | Finds the endpoints from the 401 alone | RFC 9728 and RFC 8414 metadata |
| Dynamic client registration | No hand-made OAuth app or client secret | POST /register |
| Authorization code with PKCE | Protects the code on its way back to the client | S256 only |
| A redirect URI it can receive | Loopback port for desktop or CLI apps, hosted callback for web apps | Any URI the client registered |
| Token storage and refresh | Avoids a consent screen every hour | 1-hour access, 28-day refresh |
Connect Claude Code, Cursor and Claude
Each client only needs the server URL; the OAuth steps above happen on first use.
Claude Code:
claude mcp add --transport http mcpbackend https://mcp.mcpbackend.com/mcpThen run /mcp inside Claude Code and choose to authenticate the server. A browser tab opens on the consent page.
Cursor, in ~/.cursor/mcp.json or a project's .cursor/mcp.json:
{
"mcpServers": {
"mcpbackend": { "url": "https://mcp.mcpbackend.com/mcp" }
}
}Claude and Claude Desktop: Settings → Connectors → Add custom connector, paste the URL and click Connect. Other clients that implement the MCP authorization spec should follow the same flow, but we only document the three above; check your client's own docs for where remote servers go. The MCP config generator writes the snippet for several clients, and Connect MCP and the Agent Skill covers the rest of the setup.
list_projects and create_table appear, discovery, registration, consent and token exchange all worked.API key vs OAuth for MCP servers
Plenty of MCP servers accept a static API key in a header instead of OAuth. Both work; they suit different situations.
| Static API key | OAuth 2.1 | |
|---|---|---|
| Setup | Create a key, paste it into the client config | Add the URL, approve once in the browser |
| Who it identifies | Whoever holds the key | A signed-in user and a named client |
| Lifetime | Until someone revokes it | Short access token plus refresh token |
| Where the secret lives | Config files, env vars, sometimes git | The client's token store |
| Scope | Whatever the key was created with | Scopes the user approved |
| Good for | CI jobs, server-to-server calls, local tools | Hosted servers used from AI clients |
MCPBackend uses both, for different surfaces. The MCP server accepts OAuth access tokens only, so you never paste a credential into Claude or Cursor. The REST data API your finished app calls uses project API keys (per-table permissions, shown once at creation) for servers and scripts, and end-user tokens from app signup and login for browsers. A project API key sent to /mcp is rejected with the same 401 as no token at all. The dashboard login versus app login guide explains why those identities stay separate.
Common MCP OAuth errors and fixes
| What you see | What it means | Fix |
|---|---|---|
| 401 invalid_token: Missing or invalid access token | No token, an expired one, or a credential that is not an MCP OAuth token (such as an API key) | Re-authenticate from the client; do not paste keys into the MCP config |
| 400 Invalid authorization request | The /authorize URL is malformed or lacks S256 PKCE | Update the client; plain PKCE and implicit flow are not supported |
| 400 Unknown OAuth client | The client ID was never registered here or its registration expired | Remove the server from the client and add it again so it re-registers |
| This authorization request expired or was already used | The consent page sat open over 10 minutes, or was submitted twice | Start the connection again from the client |
| Your dashboard session expired. Sign in again. | The dashboard sign-in behind the consent page lapsed | Sign in again; you return to the same consent screen |
| error=access_denied at the redirect | Deny was clicked on the consent page | Reconnect and click Connect |
| invalid_grant at /token | Code reused, PKCE verifier mismatch, or refresh token older than 28 days | Run the sign-in again from the client |
Two longer troubleshooting guides cover the cases that are hardest to spot: MCP OAuth keeps returning to consent and MCP returns unauthorized after a successful connection. The MCP guides hub has the rest.
Adding OAuth to your own MCP server
If you are building a remote MCP server rather than connecting to one, you need an authorization server that issues tokens for it. The usual options are an OAuth provider library running next to your server or an existing identity provider that supports dynamic client registration. MCPBackend runs Cloudflare's workers-oauth-provider library in the same Worker as its MCP endpoint, and hands the browser to its dashboard for sign-in and consent.
Whichever you choose, check four things before you ship: a 401 with a resource_metadatalink, metadata at both well-known URLs, S256-only PKCE, and tokens bound to your server's resource URL. If your actual goal is a database, auth and an API for an app you are building with an AI editor, you may not need your own server at all; see the remote MCP server guide and the MCP database guide.
Frequently asked questions
How does MCP authentication work?+
A remote MCP server acts as an OAuth 2.1 resource server. When a client calls it without a token, the server answers 401 with a link to its protected resource metadata. The client reads that, finds the authorization server, registers itself, sends you to a browser sign-in and consent page, exchanges the returned code for tokens using PKCE, and then sends the access token as a Bearer header on every MCP request.
Does MCP require OAuth?+
Not always. The MCP authorization spec applies to HTTP transports and is optional. Local stdio servers usually read credentials from environment variables instead. Remote servers that act on a user's account should use OAuth so the user never pastes a long-lived secret into an AI client.
What is dynamic client registration in MCP?+
Dynamic client registration (RFC 7591) lets an MCP client create its own OAuth client ID by POSTing its name and redirect URIs to the server's registration endpoint. It means you can add a server by URL alone, without creating an OAuth app by hand. MCPBackend's registration endpoint is https://mcp.mcpbackend.com/register.
Can I use an API key instead of OAuth for an MCP server?+
Some servers accept a static API key in a header, which is simpler but means a long-lived secret sits in a config file. MCPBackend's MCP server accepts OAuth access tokens only. Its project API keys are for the data API that your app calls, not for the MCP endpoint.
How long do MCP OAuth tokens last?+
It depends on the server. On MCPBackend an access token lasts one hour and a refresh token 28 days, so a client used at least once every four weeks stays connected without asking you to sign in again. Client registrations expire after 90 days.
Why does my MCP client keep asking me to authenticate?+
Usually the client cannot store or use its refresh token, the refresh token has expired, or the client registration was removed. Remove the server from the client, add it again and complete the consent screen once. If the loop continues, check that the client supports PKCE with S256 and the authorization code grant.
Build the backend, skip the setup
Give your AI editora real backend
Start with a free project. Database, REST API, auth, permissions, webhooks, and MCP are included.