Skip to content

2026-07-28 readiness: peer SDK is being retired, plus 405 / CORS / HeaderMismatch gaps #6

Description

@quotentiroler

Filing this as an issue rather than a PR because the obvious one-line change does not exist, and the real change needs a maintainer decision.

The peer dependency cannot just be bumped

The plan was "bump @modelcontextprotocol/sdk from >=1.29.0 to a beta that supports 2026-07-28." That is not possible. Verified against npm today:

$ npm view @modelcontextprotocol/sdk dist-tags --json
{ "latest": "1.29.0" }                 # no beta / next / rc tag
                                       # only prerelease ever published: 1.23.0-beta.0

$ npm view @modelcontextprotocol/server dist-tags --json
{ "latest": "2.0.0-beta.5", "beta": "2.0.0-beta.5" }

$ npm view @modelcontextprotocol/client dist-tags --json
{ "latest": "2.0.0-beta.5", "beta": "2.0.0-beta.5" }

Per the SDK beta announcement: "TypeScript v2 retires the monolithic @modelcontextprotocol/sdk package in favor of focused ones, like @modelcontextprotocol/server for servers, @modelcontextprotocol/client for clients."

So this is a peer swap, not a version bump, and it touches our public contract plus every import path:

  • peerDependencies: @modelcontextprotocol/sdk -> @modelcontextprotocol/server
  • src/transport.ts: @modelcontextprotocol/sdk/server/webStandardStreamableHttp.js, @modelcontextprotocol/sdk/server/mcp.js
  • consumers' McpServer import, i.e. every example in the README

Decision needed before any code: do we (a) cut 0.2.x on the v2 beta and leave 0.1.x on the 1.x line, (b) support both generations behind separate entry points (/v2?) with both peers optional, or (c) wait for @modelcontextprotocol/server to go stable? My read is (a), since straddling two SDK generations in one transport is how this package stops being small, but it is your call and it is breaking either way.

Transport-layer gaps, independent of the SDK

These are ours regardless of which SDK we peer on, since they are HTTP behaviour rather than protocol semantics.

1. HeaderMismatch validation is unimplemented. Spec:

Servers that process the request body MUST reject requests where the values specified in the headers do not match the corresponding values in the request body. [...] servers MUST return HTTP status 400 Bad Request and MUST include a JSON-RPC error response using [...] -32020 HeaderMismatch.

Failure conditions include a missing required header (MCP-Protocol-Version, Mcp-Method, Mcp-Name), a header that does not match the body, and invalid characters. Note Mcp-Name is required only for tools/call, resources/read and prompts/get, and its value may arrive Base64-wrapped as =?base64?...?=, which we must decode before comparing. -32020 should join JSON_RPC_ERROR_CODES in src/errors.ts.

2. GET / DELETE should be 405, not 404. The spec says a server on this revision receiving legacy traffic SHOULD respond:

HTTP GET or DELETE to the MCP endpoint: respond with 405 Method Not Allowed.

Our documented lifecycle 404s everything that is not a POST or a well-known path. This matters for real interop: clients use 400 / 404 / 405 plus the body shape to decide whether to fall back to the legacy initialize era, so a 404 here is actively misleading.

3. Default CORS exposes two headers that no longer exist. exposeHeaders defaults include Mcp-Session-Id and Last-Event-ID. Sessions are gone (SEP-2567) and Last-Event-ID resumability is gone ("Resumable SSE streams via Last-Event-ID are not supported"). Harmless but wrong, and it should not be trimmed until we decide the story for handleMcpPostStateful below.

4. handleMcpPostStateful is now a legacy-only path. It routes on Mcp-Session-Id and mints sessions on initialize (src/transport.ts), both removed in this revision. Its reason to exist was server-initiated RPC, which SEP-2322 replaces with InputRequiredResult embedded in results, and the spec is explicit that a server "MUST NOT send independent JSON-RPC requests" on a response stream. Sampling / roots / logging are deprecated on a twelve-month clock, and ping / logging/setLevel / notifications/roots/list_changed are removed now. Suggest we document it as legacy-clients-only and stop presenting it as the sampling escape hatch.

5. subscriptions/listen is the new push mechanism. The standalone GET stream is gone; long-lived change notifications now ride the response stream of a subscriptions/listen POST. Mostly SDK territory, but we should confirm our SSE cleanup in handleMcpPost does not close the server on a stream that is meant to stay open. The current code defers server.close() until the stream completes, which looks right, though a keep-alive comment line (:\r\n) is encouraged for quiet periods and we do not emit one.

The good news

The core shape needs no redesign, which was the whole bet. handleMcpPost already constructs one WebStandardStreamableHTTPServerTransport per POST with no sessionIdGenerator, and createServer(token, ctx) is already a per-request factory. Nothing above touches consumers' tools.

Suggested sequencing

  1. Land 405 + -32020 validation + an errors.ts code, all additive and testable against 1.x today.
  2. Decide the SDK strategy above, then do the peer swap and import rewrite as its own breaking release.
  3. Trim CORS defaults and re-document handleMcpPostStateful in that same breaking release.

Happy to open (1) as a PR now, since it is unblocked. (2) needs your answer first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions