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
- Land 405 +
-32020 validation + an errors.ts code, all additive and testable against 1.x today.
- Decide the SDK strategy above, then do the peer swap and import rewrite as its own breaking release.
- 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.
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/sdkfrom>=1.29.0to a beta that supports 2026-07-28." That is not possible. Verified against npm today:Per the SDK beta announcement: "TypeScript v2 retires the monolithic
@modelcontextprotocol/sdkpackage in favor of focused ones, like@modelcontextprotocol/serverfor servers,@modelcontextprotocol/clientfor 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/serversrc/transport.ts:@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js,@modelcontextprotocol/sdk/server/mcp.jsMcpServerimport, i.e. every example in the READMEDecision needed before any code: do we (a) cut
0.2.xon the v2 beta and leave0.1.xon the 1.x line, (b) support both generations behind separate entry points (/v2?) with both peers optional, or (c) wait for@modelcontextprotocol/serverto 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.
HeaderMismatchvalidation is unimplemented. Spec: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. NoteMcp-Nameis required only fortools/call,resources/readandprompts/get, and its value may arrive Base64-wrapped as=?base64?...?=, which we must decode before comparing.-32020should joinJSON_RPC_ERROR_CODESinsrc/errors.ts.2.
GET/DELETEshould be405, not404. The spec says a server on this revision receiving legacy traffic SHOULD respond:Our documented lifecycle 404s everything that is not a POST or a well-known path. This matters for real interop: clients use
400/404/405plus the body shape to decide whether to fall back to the legacyinitializeera, so a404here is actively misleading.3. Default CORS exposes two headers that no longer exist.
exposeHeadersdefaults includeMcp-Session-IdandLast-Event-ID. Sessions are gone (SEP-2567) andLast-Event-IDresumability is gone ("Resumable SSE streams viaLast-Event-IDare not supported"). Harmless but wrong, and it should not be trimmed until we decide the story forhandleMcpPostStatefulbelow.4.
handleMcpPostStatefulis now a legacy-only path. It routes onMcp-Session-Idand mints sessions oninitialize(src/transport.ts), both removed in this revision. Its reason to exist was server-initiated RPC, which SEP-2322 replaces withInputRequiredResultembedded 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, andping/logging/setLevel/notifications/roots/list_changedare removed now. Suggest we document it as legacy-clients-only and stop presenting it as the sampling escape hatch.5.
subscriptions/listenis the new push mechanism. The standalone GET stream is gone; long-lived change notifications now ride the response stream of asubscriptions/listenPOST. Mostly SDK territory, but we should confirm our SSE cleanup inhandleMcpPostdoes not close the server on a stream that is meant to stay open. The current code defersserver.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.
handleMcpPostalready constructs oneWebStandardStreamableHTTPServerTransportper POST with nosessionIdGenerator, andcreateServer(token, ctx)is already a per-request factory. Nothing above touches consumers' tools.Suggested sequencing
-32020validation + anerrors.tscode, all additive and testable against 1.x today.handleMcpPostStatefulin that same breaking release.Happy to open (1) as a PR now, since it is unblocked. (2) needs your answer first.