[mcp] HTTP transport POSTs to URL origin, ignoring configured path component
[mcp] HTTP transport POSTs to URL origin, ignoring configured path component
Summary
Claude Code 2.1.126 strips the path component from the configured mcpServers.<name>.url when initiating the MCP HTTP transport POST and when constructing the OAuth resource indicator. A server configured at https://host.tld/mcp receives POST / instead of POST /mcp, resulting in a 404 from the upstream and an unhelpful "reconnecting failed" message that hides the actual cause. The path-stripping happens in two independent code paths, so changing the configured url to include the path has no effect — there is no client-side workaround.
Environment
- Claude Code: 2.1.126 (Windows native)
- OS: Windows 11
- Transport:
type: "http"(Streamable HTTP / SSE) - MCP server: cyanheads
obsidian-mcp-server v3.1.1(any path-mounted MCP server reproduces) - Auth server: Ory Hydra (any OAuth 2.1 + PKCE provider reproduces)
Repro
.claude.json mcpServers entry:
"my-mcp": {
"type": "http",
"url": "https://mcp.example.com",
"oauth": {
"clientId": "00000000-0000-0000-0000-000000000000",
"callbackPort": 8080,
"scopes": "my.read my.write openid"
}
}
Trigger /mcp → highlight server → Reconnect.
UI reports: Got new credentials, but reconnecting to my-mcp failed. Restart Claude Code to retry.
Wire trace (nginx access.log on the MCP server)
Full Reconnect flow from Claude Code 2.1.126:
T+0 GET /.well-known/oauth-protected-resource [Bun/1.3.14] 200
T+1 POST /oauth2/revoke [axios/1.13.6] 400×2 (token still valid; harmless)
T+1 GET /.well-known/oauth-authorization-server [Bun/1.3.14] 200
T+3 GET /oauth2/auth?response_type=code&PKCE&prompt=consent
&resource=https%3A%2F%2Fmcp.example.com%2F 302
T+3 GET /login?login_challenge=... [Chrome 147] 200 ← browser opens
T+4 GET /api/login/google?login_challenge=... 303
T+7 GET /api/login/google/callback?code=... 303 ← SSO returns
T+7 GET /oauth2/auth?...&login_verifier=... 302
T+8 GET /consent?consent_challenge=... 200
T+9 POST /api/consent 303
T+9 GET /oauth2/auth?...&consent_verifier=... 303 ← code minted
T+10 POST /oauth2/token [Bun/1.3.14] 200, 2718 bytes ← NEW BEARER
T+10 POST / [claude-code/2.1.126 (cli)] ⚠️ 404, 13 bytes
Decoded token (server side, all green)
{
"iss": "https://auth.example.com",
"aud": ["https://mcp.example.com/"],
"sub": "11111111-1111-1111-1111-111111111111",
"client_id": "00000000-0000-0000-0000-000000000000",
"scp": ["my.read", "my.write", "openid", "offline_access"]
}
Token verifies cleanly. Server logs show OAuth token verification successful + Authentication successful. Auth context populated. for every request that reaches the upstream.
Confirmation that auth + server are not at fault
Same bearer, same headers, manual probe to the correct path:
curl -i -X POST https://mcp.example.com/mcp \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"manual-probe","version":"1.0"}}}'
HTTP/1.1 200 OK
Content-Type: text/event-stream
event: message
data: {"result":{"protocolVersion":"2024-11-05",
"capabilities":{"logging":{},"resources":{...},"tools":{...},"prompts":{...},"tasks":{...}},
"serverInfo":{"name":"obsidian-mcp-server","version":"3.1.1"}},
"jsonrpc":"2.0","id":1}
The only delta between the working probe and the failing Claude Code reconnect is the request path (/mcp vs /).
Root cause analysis
- The configured
urlfield washttps://mcp.example.com(no path). - Claude Code derived the OAuth resource indicator as
https://mcp.example.com/(origin + trailing slash). - Claude Code POSTed the MCP
initializerequest tohttps://mcp.example.com/(origin) — not to the protected MCP endpoint. - The protected MCP endpoint is at
/mcp. Hitting/returns 404.
The path-stripping behavior means there is no way to point Claude Code at an MCP server mounted on any path other than /. Servers that namespace MCP under /mcp, /api/mcp, etc. are unreachable from Claude Code's HTTP transport unless reverse-proxy rewrites are added on the server side as a workaround.
Confirmation: changing the configured url to include /mcp does NOT change behavior
After the first capture, the .claude.json url was changed from https://mcp.example.com to https://mcp.example.com/mcp, Claude Code was fully restarted, auth was cleared, and Reconnect was triggered to mint a fresh token from scratch.
Wire trace from the second test:
T+0 POST /oauth2/revoke [axios/1.13.6] 400×2 (Clear Auth)
T+12 GET /oauth2/auth?response_type=code&PKCE
&resource=https%3A%2F%2Fmcp.example.com%2F 302 ← path STILL stripped
T+16 GET /oauth2/auth?...&login_verifier=... 302
T+18 GET /oauth2/auth?...&consent_verifier=... 303
T+19 POST /oauth2/token [Bun/1.3.14] 200, 2718 bytes
T+19 POST / [claude-code/2.1.126 (cli)] ⚠️ 404, 13 bytes
← path STILL stripped
Decoded fresh token:
{
"iss": "https://auth.example.com",
"aud": ["https://mcp.example.com/"], // ← path stripped here too
"iat": <22 seconds ago>
}
Both code paths — the OAuth resource indicator AND the MCP transport POST target — derive from the configured url after stripping the path. There is no client-side workaround: changing the url to include the path has no effect on either output.
Expected behavior
The MCP transport POST target must preserve the path component of the configured url. If url: https://host.tld/api/mcp, the request should be POST /api/mcp — not POST /.
(Separately: the OAuth resource indicator per RFC 8707 may legitimately be either the origin or the full path; that is server policy. The two should not be conflated by the client.)
Proposed error-message improvement (independent of the fix)
On any non-2xx response from the MCP transport endpoint, surface the URL and HTTP status to the user instead of the generic "reconnecting failed":
MCP transport returned HTTP 404 from POST https://mcp.example.com/ — check that the configured URL points to your MCP endpoint.
This single change would have made the present bug self-diagnosable without nginx access-log access.
Server-side workaround (in case other users are blocked)
For path-mounted MCP servers behind nginx, add an exact-match rewrite to forward bare / POSTs to the actual MCP endpoint until the upstream client fix ships:
# Workaround for Claude Code 2.1.126 path-stripping bug — the client POSTs to
# the URL origin instead of the configured /mcp path. Exact-match takes priority
# over the prefix location below, so only bare "/" is rewritten; "/mcp",
# "/.well-known/*", etc. continue to flow through unchanged.
# Remove this block once the upstream client fix ships.
location = / {
proxy_pass http://127.0.0.1:<upstream-port>/mcp;
}
location / {
proxy_pass http://127.0.0.1:<upstream-port>;
}
Confirmed working: POST / and POST /mcp both return the expected 200 OK SSE initialize response after this rewrite.
Suggested labels
mcp, bug, transport, dx
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗