Skip to content

feat(mcp): explain the CORS failure the browser refuses to explain - #8

Merged
siarheidudko merged 1 commit into
mainfrom
claude/mcp-remote-http-oauth-ges1mj
Aug 16, 2026
Merged

feat(mcp): explain the CORS failure the browser refuses to explain#8
siarheidudko merged 1 commit into
mainfrom
claude/mcp-remote-http-oauth-ges1mj

Conversation

@siarheidudko

Copy link
Copy Markdown
Member

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-Version on every request after initialize since 2025-06-18. The first request does not carry it, so discovery, the token exchange and initialize all succeed against a server whose Access-Control-Allow-Headers predates 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 POST returned a readable 401, the same POST with mcp-protocol-version was blocked outright.

What changes

connectMcpHttp, on a network-level failure, re-probes the endpoint with a request carrying only content-type — enough to need the same preflight, asking for nothing else. The answer separates the two cases:

plain probe conclusion reported
gets through origin is allowed; a header is refused → name MCP-Protocol-Version, list what Access-Control-Allow-Headers needs
blocked too the origin is refused → point at Access-Control-Allow-Origin, OPTIONS, and the SDK's allowedOrigins

A 401 whose WWW-Authenticate is 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 surface result.error verbatim, 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 Authorization in Allow-Headers and two exposed headers. That was incomplete in the way that costs the most: it did not mention MCP-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 the 403 Origin not allowed an empty allowedOrigins produces for every browser request.

Tests

Six, over a stubbed fetch on example hostnames — no live endpoint involved:

  • the refused header is named when the plain probe succeeds, and the probe itself is asserted to send only content-type (a probe that asks for more proves nothing)
  • a blocked plain probe points at the origin instead, and does not mention the header
  • an unreadable 401 challenge is called out; a readable one stays quiet
  • connectMcpHttp attaches the diagnosis to the reported error
  • an ordinary 500 is not dressed up as a CORS problem

typecheck, format:check, build, test green (96 pass).


Generated by Claude Code

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
@siarheidudko
siarheidudko marked this pull request as ready for review August 16, 2026 20:30
@siarheidudko
siarheidudko merged commit 893b9c3 into main Aug 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants