Skip to content

Pinned protocol version is sent as a duplicate header ("2025-11-25, 2026-07-28"), breaking connect with a 502 #3453

Description

@stijnhaus

Summary

Setting Connection overrides → Protocol version to a specific value (e.g. 2026 RC (2026-07-28)) on a remote HTTPS server makes the client send two values in one MCP-Protocol-Version header, comma-joined:

MCP-Protocol-Version: 2025-11-25, 2026-07-28

Spec-compliant servers match that header exactly, so they reject it as an unsupported version and return 400. The hosted app surfaces this as a Request failed (502) toast from its own proxy, which makes it look like a server outage rather than a malformed request. The pin feature is therefore unusable against
any server that validates the header.

Steps to reproduce

  1. Add a remote HTTPS MCP server (OAuth auth) in the hosted app.
  2. ⋮ → ConfigureConnection overridesProtocol version2026 RC (2026-07-28) → Save Changes.
  3. Toggle the server on.

Expected: the client sends MCP-Protocol-Version: 2026-07-28 and connects.
Actual: Request failed (502). Server-side, the request arrives with MCP-Protocol-Version: '2025-11-25, 2026-07-28' and is rejected as an unsupported version. A second connect attempt never reaches the server at all (client-side short-circuit).

Setting Protocol version back to Client default connects instantly against the same server, same credentials — so this is specific to the pin path, not the server or the OAuth session.

Root cause

StreamableHTTPClientTransport._commonHeaders (@modelcontextprotocol/client, observed on 2.0.0-beta.4) sets the header lowercase and then spreads the caller's connection-level requestInit.headers verbatim:

new Headers({ "mcp-protocol-version": negotiated, ...requestInit.headers })

Headers treats field names case-insensitively but an object literal does not de-duplicate across different casings, so a canonical-cased MCP-Protocol-Version supplied by the caller appends instead of replacing. Reproducible standalone on Node 24:

new Headers({ "mcp-protocol-version": "2025-11-25", "MCP-Protocol-Version": "2026-07-28" }).get("mcp-protocol-version")
// => "2025-11-25, 2026-07-28"

The SDK's RESERVED_REQUEST_HEADER_NAMES guard covers per-request options.headers only — not connection-level requestInit.headers — so nothing upstream catches it.

The app used to have its own case-insensitive de-dup that prevented this; it was removed in #3317 (merged 2026-07-20) and not replaced. The per-server pin UI then landed in #3365 (merged 2026-07-22), which is when the combination became reachable.

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