Repository navigation
Web client: protocol era "auto" does not fall back to legacy when a 2025-only server rejects the probe with HTTP 400 #2660
Description
Which version line?
v2 (@modelcontextprotocol/inspector@latest)
Which client?
Web
Inspector version
2.10.1
Node version
v24.21.0
Operating system (and browser, for the web client)
Linux x86_64, Chrome 154
Transport
Streamable HTTP
MCP server under inspection
A stateless Streamable HTTP server built on @modelcontextprotocol/sdk 1.32.1 (so it only speaks the 2025-era protocol). It is the stateless pattern from the v1 simpleStatelessStreamableHttp example: sessionIdGenerator: undefined, enableJsonResponse: true, a fresh server per POST.
Protocol era: auto
OAuth: no
package.json:
{
"name": "inspector-auto-v1-repro",
"private": true,
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "1.32.1",
"@modelcontextprotocol/inspector": "2.10.1"
}
}server.mjs:
import { createServer } from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
createServer(async (req, res) => {
if (req.method !== "POST") return res.writeHead(405).end();
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
res.on("finish", () => console.log(`${body.method} -> HTTP ${res.statusCode}`));
const server = new McpServer({ name: "v1-server", version: "1.0.0" });
server.registerTool("ping", { description: "ping" }, async () => ({ content: [{ type: "text", text: "pong" }] }));
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
await server.connect(transport);
await transport.handleRequest(req, res, body);
}).listen(3333, () => console.log("v1 server on http://localhost:3333/mcp"));Steps to reproduce
npm install, thennode server.mjsnpx mcp-inspector --web --server-url http://localhost:3333/mcp --transport http --protocol-era auto- Toggle the server's connect switch in the web UI
For comparison, the CLI against the same server and era connects fine:
npx mcp-inspector --cli http://localhost:3333/mcp --transport http --protocol-era auto --method tools/list
Expected behavior
The web client falls back to the 2025 initialize handshake and connects, as the CLI does. The v1 server rejects the server/discover probe with an HTTP 400, and the SDK's probe policy treats a 4xx other than 401/403 as a legacy signal.
Actual behavior
The connection goes to "Failed" after the probe. No initialize is sent. The toast reads:
Failed to connect to "localhost:3333"
Version negotiation probe failed: Error POSTing to endpoint: {"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"},"id":null}
Server log for the web attempt followed by the CLI run:
server/discover -> HTTP 400 <- web: nothing after this
server/discover -> HTTP 400 <- CLI
initialize -> HTTP 200
notifications/initialized -> HTTP 202
tools/list -> HTTP 200
So "auto" in the web client cannot connect to any 2025-only Streamable HTTP server built on SDK v1 stateless mode, which is the case auto is meant to cover.
Logs, errors, or screenshots
The Network panel shows the single probe: POST with mcp-protocol-version: 2026-07-28 and mcp-method: server/discover, answered with 400 BAD REQUEST, content-type: application/json and the JSON-RPC error body above ("id": null).
A possible cause, from reading the bundled client rather than from debugging it: the "Version negotiation probe failed:" prefix comes from the branch that handles a probe that threw a transport error, which in a browser only falls back for a TypeError. The branch that classifies an HTTP response by status (401/403 abort, 5xx abort, anything else legacy) is not reached. The CLI hits the same 400 with the same SDK and falls back, so the HTTP status may be lost on the web client's path between the backend and the browser before the SDK classifies the probe.
Before you submit
- I searched existing issues and this is not a duplicate.
- This is not a security vulnerability report (those go through the private advisory process).
Activity
jarrettdustinqq commentedon Oct 9, 2026
Source-level confirmation on current v2 (I have not rerun the reproduction locally):
core/mcp/remote/node/server.tscatches every non-AuthChallengeErrorfrom the upstreamtransport.send()/ response wait and serializes only{ ok: false, kind: "transport_error", error: msg }.core/mcp/remote/remoteClientTransport.tsreceives that envelope and rethrowsnew Error(json.error).That relay therefore drops the original error class plus any structured
status/causechain before the browser-side SDK classifies theserver/discoverfailure. The direct CLI path bypasses this relay, which fits the reported CLI/web difference.A narrow regression test should go through the browser remote transport (not only
createTransportNode), make the upstream discovery probe return HTTP 400, and assert thatinitializeis sent afterward. The repair should preserve a typed, bounded wire representation of the classification data and reconstruct it at the browser boundary; parsing the status back out of the message would be fragile because the upstream body is arbitrary server text.Relevant source at the current head:
inspector/core/mcp/remote/node/server.ts
Lines 1071 to 1085 in 0d2bc1d
inspector/core/mcp/remote/remoteClientTransport.ts
Lines 786 to 827 in 0d2bc1d
inspector/core/mcp/remote/types.ts
Lines 92 to 105 in 0d2bc1d
AI-assisted source review; no local runtime verification in this environment.
jarrettdustinqq commentedon Oct 9, 2026
Correction to the first and third source ranges above (the analysis is unchanged):
inspector/core/mcp/remote/node/server.ts
Lines 1100 to 1113 in 0d2bc1d
RemoteSendResponsetransport-error variant is atinspector/core/mcp/remote/types.ts
Lines 106 to 117 in 0d2bc1d
cliffhall commentedon Oct 12, 2026
Triage: Priority High (total 9)
auto, to a 2025-only stateless Streamable HTTP server (the caseautoexists for); the CLI handles the same serverbuglabelBoard: #28, Status
Incoming(awaiting maintainer review — no milestone yet).cliffhall commentedon Oct 12, 2026
Fixed by #2684, merged into v2/main.
fix(remote): relay upstream HTTP status so auto era falls back on a 4…