feat(mcp): explain the CORS failure the browser refuses to explain - #8
Merged
Merged
Conversation
A CORS rejection reaches JavaScript as a bare `TypeError: Failed to fetch`, indistinguishable from a server being down; the reason goes to the devtools console and nowhere a program can read it. The connector now works it out instead of repeating the silence. On a network-level failure it re-probes the endpoint with a request carrying only `content-type`. If that gets through, the origin is fine and a request HEADER is being refused — and the header the transport adds is `MCP-Protocol-Version`, required on every request after `initialize` since spec 2025-06-18 and missing from CORS lists written before it. That is the shape of the bug where discovery, the OAuth round-trip and `initialize` all succeed and the connection dies moments later, reading exactly like the consent step failed. If the plain probe is blocked too, the origin itself is refused, which is a different sentence. A 401 whose challenge is not exposed gets called out while we are in there. `diagnoseMcpCors` is exported so a UI can say it too, and the README now lists every header a server must send, with what each one costs when absent. Costs nothing in normal operation: the probe runs only on the failure path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FKop4At26QqqkwVGEjJur
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The failure this is about
Connecting a browser MCP client to a remote server, the OAuth round-trip completes, the consent screen closes, and then the connection dies with nothing useful said. It reads as "the consent step broke", so the search starts in the client — which is the wrong place.
It is CORS, and the browser is deliberately unhelpful about it: a rejected request reaches JavaScript as a bare
TypeError: Failed to fetch, identical to the server being down. The reason is printed to the devtools console and exposed nowhere a program can read.The specific trap: the spec has required
MCP-Protocol-Versionon every request afterinitializesince 2025-06-18. The first request does not carry it, so discovery, the token exchange andinitializeall succeed against a server whoseAccess-Control-Allow-Headerspredates that requirement — and then every request afterwards is blocked. The success runs exactly far enough to look like the OAuth part worked and something later fell over.I hit this against two live deployments while debugging a report of "hangs after the consent screen", and reproduced it in a real browser: a plain
POSTreturned a readable 401, the samePOSTwithmcp-protocol-versionwas blocked outright.What changes
connectMcpHttp, on a network-level failure, re-probes the endpoint with a request carrying onlycontent-type— enough to need the same preflight, asking for nothing else. The answer separates the two cases:MCP-Protocol-Version, list whatAccess-Control-Allow-HeadersneedsAccess-Control-Allow-Origin,OPTIONS, and the SDK'sallowedOriginsA 401 whose
WWW-Authenticateis not exposed gets called out in passing, since a browser client cannot start discovery without reading it.diagnoseMcpCors(url, fetch?)is exported so a UI can print the same sentence rather than "failed to connect". The React bindings already surfaceresult.errorverbatim, so the demo panel picks this up with no change.The probe runs only on the failure path — normal connects are untouched.
README
The MCP section previously asked for
Authorizationin Allow-Headers and two exposed headers. That was incomplete in the way that costs the most: it did not mentionMCP-Protocol-Version, so a reader following it exactly still hits this. It now gives the full header block and what each line costs when absent, including the403 Origin not allowedan emptyallowedOriginsproduces for every browser request.Tests
Six, over a stubbed fetch on example hostnames — no live endpoint involved:
content-type(a probe that asks for more proves nothing)connectMcpHttpattaches the diagnosis to the reported errortypecheck,format:check,build,testgreen (96 pass).Generated by Claude Code