Skip to content

Fix structured HTTP error classification during server/discover fallback #1790

Description

@PranavSenthilnathan

Description

Two related gaps remain in the HTTP backward-compatibility handling for the server/discover probe.

1. Structured HTTP 400 with -32600 can skip initialize fallback

A session-enforcing pre-SEP-2575 server can respond to the initial server/discover request with:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "Unknown or missing Mcp-Session-Id. Send initialize first."
  }
}

StreamableHttpClientSessionTransport correctly converts the structured HTTP 400 response to McpProtocolException. However, McpClientImpl.ConnectAsync has a special catch for InvalidRequest errors whose message contains Mcp-Session-Id. That catch describes the error as locally generated and rethrows it, even though client-side protocol exceptions currently originate from remote JSON-RPC responses. The remote error therefore never reaches the generic McpProtocolException branch that falls back to initialize.

The 2026-07-28 Streamable HTTP compatibility rules identify only -32020 (HeaderMismatch), -32021 (MissingRequiredClientCapability), and -32022 (UnsupportedProtocolVersion) as recognized modern MCP errors. A structured -32600 response is not a recognized modern error and should fall back to initialize, regardless of message text.

The special message-based catch appears stale: there are no local client-side McpProtocolException throw sites on current main, and none were found in the commit that introduced the guard. It should be removed unless a concrete local producer still requires distinct handling.

2. AutoDetect loses HTTP status semantics for structured non-400 responses

AutoDetectingClientSessionTransport converts any unsuccessful response containing a JSON-RPC error envelope into McpProtocolException, regardless of HTTP status. A structured 401, 403, or 500 carrying a non-modern error can consequently reach the generic protocol-error branch and incorrectly trigger initialize fallback.

The body-based backward-compatibility decision applies to HTTP 400. Other HTTP failures should retain their HTTP semantics unless the body contains one of the recognized modern MCP errors.

Expected behavior

Use a shared classification rule in the Streamable HTTP and AutoDetect transports:

  • HTTP 400 with any JSON-RPC error: surface it as McpProtocolException; connection logic handles recognized modern errors or falls back for other errors.
  • Any HTTP status with -32020, -32021, or -32022: surface the corresponding protocol exception.
  • Any other HTTP status with any other JSON-RPC error: surface HttpRequestException while preserving the status and response body.
  • A structured JSON-RPC response proves the endpoint speaks Streamable HTTP, so AutoDetect should not attempt deprecated SSE solely because the response is an HTTP failure.

Suggested tests

Cover both HttpTransportMode.StreamableHttp and HttpTransportMode.AutoDetect where applicable:

  • Structured HTTP 400 + -32600 mentioning Mcp-Session-Id falls back to initialize.
  • Structured HTTP 400 + other non-modern JSON-RPC errors falls back.
  • Structured HTTP 400 + -32020, -32021, or -32022 retains modern handling.
  • Structured HTTP 401, 403, and 500 under AutoDetect surface HttpRequestException with the original status and do not send initialize or attempt SSE.

Related: #1765, #1766, #1767, #1768.

Note

This issue was drafted with GitHub Copilot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions