The honest version of my MCP server's auth story is short: there wasn't any. The server ran over HTTP, anyone who knew the URL could call its tools, and the tools touched a real account. I knew this was bad. I had even written about other people doing it. So I did the responsible thing and added OAuth 2.1 the way the MCP spec describes it.
Then I connected the same server from five clients to confirm nothing broke. Two of them authorized cleanly. Three of them broke, each in a completely different place, and not one of the failures was a bug in my server. The spec was satisfied. The clients had simply not caught up to it. That gap, the few inches between "the spec says X" and "the client does X," is exactly where your authorization lives, which means it is exactly the place you cannot afford a gap.
What "adding OAuth to an MCP server" actually means in 2026
Adding OAuth to an MCP server in 2026 means implementing the 2025-06-18 authorization spec, where your server is an OAuth 2.1 Resource Server, not its own authorization server. This is the part that surprised me, because it inverts the older mental model.
In the previous revision (2025-03-26) the MCP server was treated more or less as its own authorization server. The 2025-06-18 changelog reclassified it: "Classify MCP servers as OAuth Resource Servers, adding protected resource metadata to discover the corresponding Authorization server." So my server's job got smaller and pickier at the same time. It no longer issues tokens. It validates them, advertises where the real authorization server lives, and rejects anything addressed to someone else.
Concretely, a compliant server has to do four things:
- Return
401 Unauthorizedwith aWWW-Authenticateheader pointing at its resource metadata URL. - Serve Protected Resource Metadata at
/.well-known/oauth-protected-resource(RFC 9728), naming the authorization server. - Let the client discover the authorization server's own metadata (RFC 8414) and register through Dynamic Client Registration (RFC 7591), which the spec marks as SHOULD, not MUST.
- Validate the token's audience so a token minted for some other service can't be replayed against mine (RFC 8707 resource indicators).
I implemented all four. I ran the MCP Inspector against it and watched the full dance: 401, metadata fetch, browser redirect, token, authorized call. Green across the board. That was the moment I assumed I was done, which is the exact moment in every war story where the narrator is about to learn something.
The test: five clients, one server, same handshake
My test was deliberately boring. One server, five clients, the same OAuth handshake each time. The clients were the ones I actually use or expect my users to use: Claude Desktop, VS Code with the Copilot MCP integration, Cursor, Claude Code, and a remote bridge (mcp-remote) for anything that only speaks stdio.
I went in expecting an even spread of minor papercuts. What I got was a clean split. Two clients walked through the front door. Three got stuck in three different doorways, and the interesting part is that the three failures map almost perfectly onto the three things the spec leaves optional or underspecified. The clients didn't fail randomly. They failed exactly where the spec gives them room to disagree.
Here is the scoreboard before the autopsies.
Client Result Failed at
----------------------------------------------------------
Claude Desktop PASS (clean)
MCP Inspector PASS (clean)
VS Code (Copilot) BROKE loopback redirect URI port
Cursor BROKE dynamic client registration
Claude Code BROKE manual client_id rejected
(MCP Inspector is a tool, not a shipping client, but it earned its row by being the only thing that worked on the first try, which tells you something about who the spec was written against.)
Break #1: VS Code and the loopback port that wasn't there
VS Code broke because its OAuth callback wanted a specific loopback port that was already taken, and the authorization server refused the redirect. The error on screen was "Invalid Redirect URI," which is an unhelpful thing to read when your redirect URI looks correct.
VS Code added native MCP authentication in version 1.101: it parses WWW-Authenticate, follows the metadata, and tries DCR first. All correct. The trouble is the redirect target. OAuth for a desktop client uses a loopback redirect (http://127.0.0.1:<port>/...), and RFC 8252 ยง7.3 is explicit that the authorization server MUST allow any port for loopback, because the client picks an ephemeral one at runtime. Plenty of authorization servers ignore that and do strict, exact redirect-URI matching including the port. When the port VS Code grabbed didn't match what was registered, the handshake died.
This is not a hypothetical I reverse-engineered from my own logs. It is filed: microsoft/vscode #278512, "MCP: OAuth Server Redirect URI Mismatch Bug," describes the default loopback port being unavailable and producing exactly "Invalid Redirect URI." The same shape shows up in github/copilot-cli #1951 and in claude-code #51319, where a hardcoded callback port breaks the moment you run two sessions at once. The fix lives on the authorization server: relax redirect matching for loopback the way the RFC tells you to. But you only learn you needed that fix by watching a client fail, because the spec-compliant server and the spec-compliant client can still disagree about whether "port" is part of the URI.
Break #2: Cursor and the authorization server that wouldn't register a stranger
Cursor broke because it insisted on Dynamic Client Registration, and my authorization server, like most authorization servers, doesn't offer it. DCR is the step where a brand-new client introduces itself to the authorization server at runtime and gets credentials on the spot, no human pre-registration. The MCP spec lists DCR as SHOULD. Clients read SHOULD as "rely on it." Authorization servers read SHOULD as "skip it."
That mismatch is the single most common way OAuth-enabled MCP servers break, and it is not subtle in the issue trackers. github/github-mcp-server #1404: "Dynamic Client Registration not supported." claude-code #38102: the client "attempts DCR even when a clientId is supplied." openai/codex #15818: OAuth login "fails when authorization server does not support dynamic client registration." microsoft/vscode #279955, filed flatly as "Dynamic Client Registration (DCR) issues in MCP Servers." Every major client has a version of this.
The root cause is an asymmetry the spec bets on: it assumes authorization servers will support DCR, and most of them do not. Okta, Entra ID, and the rest of the enterprise identity world mostly want you to pre-register a client by hand. So a client that only knows how to DCR is a client that cannot talk to the authorization server every enterprise already runs. I had wired my server to a real identity provider, not a toy one, and the toy was the only thing that would have worked unmodified.
Break #3: Claude Code and the client_id it refused to use
Claude Code broke in the mirror image of break #2: I tried to hand it a pre-registered client_id to dodge the DCR problem, and it tried to do DCR anyway. The whole reason to support a manual client_id is to work with authorization servers that don't do DCR. A client that ignores the client_id you gave it and reaches for DCR regardless has, in effect, no escape hatch.
This one is also documented rather than imagined. claude-code #38102 is titled, almost word for word, "MCP OAuth: 'does not support dynamic client registration' despite clientId configured." On the VS Code side the gap is tracked as a feature that doesn't exist yet: #252892, "capability to register a clientId for MCP OAuth," and #257415, "No option to disable Dynamic Client Registration and use custom (static) client information." Anthropic's own connector docs say the quiet part directly for Claude.ai: it "requires Dynamic Client Registration support and does not yet support a way for users to specify a client ID or secret."
So the two failures are a pincer. Break #2 is a client that demands DCR from an authorization server that can't do it. Break #3 is a client that won't accept the manual credential you'd use to route around DCR. Same root, opposite ends, and between them they cover most of the identity providers a real company actually uses.
How I got from three breaks to zero
I got from three breaks to zero by fixing the authorization server, not the clients, plus one bridge for the client I couldn't fix. The clients aren't mine to patch, so the move was to make the server tolerant of the ways clients disagree.
- For the loopback port mismatch: relax redirect-URI matching on the authorization server so any loopback port validates, per RFC 8252. This is a server-side allowance, and it's the one change that quietly fixes a whole family of "Invalid Redirect URI" reports at once.
- For the DCR-required client against a no-DCR authorization server: put an OAuth proxy in front that does speak DCR, registers the client dynamically, and maps it onto a pre-registered upstream client. This is precisely why OAuth proxies exist in the MCP world, and why the community keeps building them.
-
For the client that ignored my manual client_id: route it through
mcp-remote, the npx bridge that runs the OAuth flow on the client's behalf and, in its static-config form, lets you inject a pre-registered client_id the picky client never had to know about.
None of these are elegant. They are shims around the seam between a spec and its implementations. But they share a useful property: every fix lives on infrastructure I control, because the one thing you can't ship is "please upgrade your client."
What I'd tell you before you add OAuth to an MCP server
Before you add OAuth to an MCP server, assume the spec is the easy part and client compatibility is the actual project. I budgeted my time backwards. The server-side OAuth 2.1 implementation took an afternoon. Reconciling it with how five clients actually behave took the rest of the week, and that ratio is the real lesson.
Three things I'd bolt to the wall:
Test against real clients, not just the Inspector. The Inspector is a reference implementation and it passes by construction. It is the most optimistic possible reader of your server. Your users are running Cursor and VS Code and Claude Code, and those are the readers who disagree. A green Inspector run means your server is correct, not that anyone can connect to it.
Treat DCR as a coin flip, and own both sides. Half the clients assume Dynamic Client Registration works; half the authorization servers don't offer it. You cannot pick a side and be safe, because your users bring their own clients and your company brings its own identity provider. The durable answer is a proxy that speaks DCR outward and pre-registration inward, so the disagreement resolves on your infrastructure instead of in an error dialog.
Know which spec revision your clients are reading. The auth spec is moving fast. The 2025-11-25 revision already deprecates DCR in favor of Client ID Metadata Documents and makes PKCE mandatory, which means a year from now the breakage will have a new shape and the same cause: clients catching up to the spec at different speeds. The thing that broke three of my five clients wasn't a flaw in OAuth or in MCP. It was the lag, and lag is permanent.
If you want the full map of where MCP's attack surface actually lives, including why so many servers ship with no auth at all and what the OWASP MCP Top 10 says to do about it, I wrote MCP Security in Practice. This post is the chapter that only exists because I tried to fix one of those gaps and found three more.
The takeaway
Adding OAuth to an MCP server is not a single task; it is a server task you finish in an afternoon followed by a compatibility task you finish never, because the clients keep moving. My server was spec-correct the whole time. Three of five clients still couldn't connect, because each one disagreed with the spec in a different place: a loopback port, a missing DCR, a rejected client_id. The fixes all lived on my side, which is the good news and the bad news at once. The good news is you can fix it. The bad news is "it works in the Inspector" was never the finish line. The finish line is five clients, and the spec only gets you to the door.

Top comments (0)