Skip to content

mcp: derive Mcp-Protocol-Version header from the outgoing message#1107

Open
jan-xyz wants to merge 1 commit into
modelcontextprotocol:mainfrom
jan-xyz:fix/streamable-protocol-version-header-wrapped-connections
Open

mcp: derive Mcp-Protocol-Version header from the outgoing message#1107
jan-xyz wants to merge 1 commit into
modelcontextprotocol:mainfrom
jan-xyz:fix/streamable-protocol-version-header-wrapped-connections

Conversation

@jan-xyz

@jan-xyz jan-xyz commented Jul 23, 2026

Copy link
Copy Markdown

Why

streamableClientConn.setMCPHeaders sets Mcp-Protocol-Version only from c.initializedResult, which is populated exclusively through the unexported clientConnection.sessionUpdated hook. Client.Connect reaches that hook via a type assertion on the Connection returned by the Transport:

  if hc, ok := cs.mcpConn.(clientConnection); ok {
      hc.sessionUpdated(cs.state)
  }

That assertion silently fails whenever the Connection is wrapped by another type that exposes only the exported Connection interface. c.initializedResult then stays nil for the life of the session, and every SEP-2575 (>= 2026-07-28) request sent through that connection is missing the header. Servers that validate the header per spec reject the request with 400 Bad Request, closing the connection.

What

Every SEP-2575 request already carries its own _meta.protocolVersion field via injectRequestMeta. setMCPHeaders
now reads the protocol version from the outgoing message first, falling back to c.initializedResult and then the request context.

This is currently breaking the GitHub MCP with the 1.7-pre releases of this module.

streamableClientConn only set the Mcp-Protocol-Version header from
c.initializedResult, which is populated exclusively through the
unexported clientConnection.sessionUpdated hook. That hook is invoked
only via a type assertion on the Connection returned by a Transport
(see Client.Connect), so it is silently skipped whenever that
Connection is wrapped by another type exposing just the exported
Connection interface: Go does not promote unexported interface
methods across an embedded interface boundary. This is a common
pattern for transports that need to intercept traffic, e.g. to filter
notifications. c.initializedResult then stays nil for the life of the
session.

This silently breaks every SEP-2575 (>= 2026-07-28) request sent
through such a wrapped connection, since servers that validate the
header per spec reject the request with 400 Bad Request, closing the
connection.

Every SEP-2575 request already carries its own _meta.protocolVersion
field. Reading the header value from there when a message is
available is a more reliable source of truth than initializedResult
and fixes the header regardless of how the Connection was
constructed.

Co-authored-by: eu.anthropic.claude-sonnet-5 via Crush <charmcrush@users.noreply.github.com>
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.

1 participant