Skip to content

Move to the v2 TypeScript SDK, then to protocol 2026-07-28 — two steps, v0.27 or later #1025

Open

Description

@debugmcpdev

Why

mcp-debugger speaks MCP protocol 2025-11-25 at most, because @modelcontextprotocol/sdk (the v1 line, latest 1.32.2) tops out there. The 2026-07-28 revision ships only in the v2 TypeScript SDK, released 2026-10-23 under new package names (@modelcontextprotocol/server, client, core, the node/express/hono/fastify transport packages, server-legacy for the frozen SSE transport), so Dependabot never offers it as a bump and nothing in the repo tracks it. Found while reviewing #1017 and building #1023.

The SDK's own upgrade guide separates the two moves, and so does this ticket: the API migration has a codemod and keeps the 2025 wire protocol; adopting 2026-07-28 is architectural. There is no deadline pressure — clients negotiate down, and the legacy era stays supported for at least twelve months under the spec's feature lifecycle policy — but the longer step 2 waits, the more code accrues on a session concept the spec has deleted.

Not before v0.26.0 ships. Target v0.27 or later; step 1 can be its own release.

Step 1 — SDK v2 on the 2025 wire protocol (mechanical, codemod-assisted)

Surface today: 16 files under src/ import the SDK (17 import sites from types.js, 5 from server/index.js, stdio/streamableHttp/sse server transports, and the client transports the dev proxy and self-debug paths use), 1 under tools/, 108 under tests/. No direct zod dependency; v2 needs zod ^4.2.0 as a peer (Zod 4.0–4.1 falls back with a warning and drops .describe() text).

  • npx @modelcontextprotocol/codemod@latest v1-to-v2 . at the repo root (it rewrites package.json too; in the monorepo it edits the nearest manifest only, so check packages/mcp-debugger and tools/dev-proxy by hand). Then grep for @mcp-codemod-error, typecheck, lint, test.
  • Package mapping: sdk/server/* → @modelcontextprotocol/server; sdk/client/* → @modelcontextprotocol/client; sdk/types.js schemas → @modelcontextprotocol/core (shared types re-exported from client/server); StreamableHTTPServerTransport → NodeStreamableHTTPServerTransport from @modelcontextprotocol/node; stdio on the /stdio subpaths.
  • setRequestHandler takes a method string, not a Zod schema, and the handler's second argument is ctx not extra (src/server/output-resources.ts, prompts.ts, the tool dispatch); registerResource needs a metadata argument; resources/read unknown-URI error becomes -32602.
  • SSEServerTransport is removed from v2: the deprecated sse command moves to @modelcontextprotocol/server-legacy/sse (frozen v1 copy) or is dropped — decide, and say so in the changelog either way.
  • createMcpHandler does not validate Host/Origin: the existing allowlist middleware stays in front of it (it already runs before handleRequest).
  • Node 20+ (we require 22), ESM-first with a CJS build; the packages/mcp-debugger bundle and scripts/bundle.js need a re-check of what esbuild pulls in.
  • The dev proxy (tools/dev-proxy) uses both a server and a client transport; the 1.x and 2.x SDKs share no classes, so it moves in the same PR.

Done when: tests/integration/cli/http-spec-contract.test.ts (the 2025-11-25 clauses, #1023) and the mcp-conformance CI lane are green unchanged; the stdio, http and container smokes pass; the published tarball still speaks 2025-11-25 to Claude Code, Codex, pi and MCP Inspector.

Step 2 — protocol 2026-07-28 (architectural)

What the revision changes, and what in this server sits on each change:

2026-07-28 change SEP What it touches here
Protocol-level sessions and Mcp-Session-Id removed; cross-call state via server-minted handles passed as tool arguments SEP-2567 the one-DebugMcpServer-per-client model (createHttpApp's session map), per-client isolation of debug sessions (#939, the foreign-session gate), the stale-session reaper (#337, #658), /health's session listing, the 404 answer (#1017). Debug session ids are already handles, so the model becomes one shared server with explicit ownership tokens, or no isolation — a design decision
initialize handshake removed; _meta carries version and client capabilities; server/discover is mandatory SEP-2575 DebugMcpServer capabilities/instructions, the mode-gated instructions (#271), list_supported_languages's per-handshake behaviour
GET SSE stream and resources/subscribe/unsubscribe replaced by subscriptions/listen SEP-2575 src/server/output-resources.ts (the output transcript subscriptions and their coalesced resources/updated), the dev proxy's stream handling, the stream-lost reaper path (#658)
ping, logging/setLevel, roots list-changed removed; logging/sampling/roots deprecated SEP-2575, SEP-2577 nothing declared today beyond ping (SDK-provided); confirm /health stays the liveness probe
HTTP+SSE transport formally Deprecated SEP-2596 the sse command (decided in step 1)
Every result carries resultType; list/read results carry ttlMs/cacheScope; tools/list order must be deterministic SEP-2322, SEP-2549 the tool result envelope (src/server/tool-result.ts), the 24 tool-list snapshots, check-docs counts
Mcp-Method/Mcp-Name headers required on POST; x-mcp-header SEP-2243 the allowlist/CORS middleware's allowed and exposed headers
SSE resumability and Last-Event-ID removed SEP-2575 CORS exposed headers; the expose_session mirror is DAP, unaffected
Error-code repartition (-32020…-32099 reserved for the spec) — the bare -32001 Session not found from #1017 and the -32000 allowlist answers are grandfathered implementation-defined; nothing to renumber unless the SDK's own bodies change

Serve both eras during the window: v2's createMcpHandler(factory, { legacy: 'stateless' }) answers 2025-era requests per request and serveStdio(factory) pins a stdio connection's era on its opening exchange; legacy: 'reject' turns the old era off later. A modern-era client needs a second contract file (http-spec-contract-2026.test.ts or a table keyed by era) — the 2025 file stays as the legacy pin until the era is retired.

Done when: a 2026-07-28 client (the conformance CLI's 0.2 line with --spec-version 2026-07-28, once it is stable) and a 2025-11-25 client both drive a full debug session over http and stdio; the Docker image, the npx bundle and the pi package pass their smokes; docs name which era each transport speaks.

Sources

Activity

  1. debugmcpdev commented on Nov 2, 2026

    @debugmcpdev
    CollaboratorAuthor

    One sequencing question for the 1.0 criteria, raised in the 2026-11-02 backlog review: step 2 removes protocol-level sessions, and the body already names the fork — "one shared server with explicit ownership tokens, or no isolation". If ownership tokens become tool arguments, that is a tool-schema change. The ROADMAP freezes the schema at 1.0, so either 1.0 waits for step 2, or step 2 is designed so ownership never surfaces as a tool parameter (handles minted per client inside the server, the way debug session ids already are). Worth deciding before the freeze is declared; nothing changes in the plan here.

  2. debugmcpdev commented on Nov 3, 2026

    @debugmcpdev
    CollaboratorAuthor

    Decided 2026-11-03, answering the 2026-11-02 question above: the handle is the capability. Debug-session ownership stays server-side and never surfaces as a tool parameter.

    Why this and not the alternatives:

    • A debug session id is already an unguessable, server-minted UUIDv4 passed as an ordinary tool argument — exactly the model SEP-2567 names ("server-minted handles passed as ordinary tool arguments"). stdio and the dev proxy already run one shared server with no further isolation, and expose_session already treats a token as the capability.
    • An owner token as a tool argument would change the schema of every session-taking tool, so 1.0 would wait for step 2 — and buy isolation that is not a security boundary on an unauthenticated server that launches arbitrary executables.
    • A _meta.clientInfo partition is self-asserted and non-unique: two instances of the same client share it.
    • Deferring leaves open the one question that gates the freeze.

    Consequences for step 2: one shared server per process in the modern era. The per-client DebugMcpServer and the #939 foreign-session gate are legacy-era behaviour, not a contract. Two guardrails, written once in the session-tree design doc (#1005 part B, the ownership & lifetime section) and reused here:

    1. list_debug_sessions is the whole process's list; the session name is the agent's own namespace hint. Several agents against one http process are mutually visible and closable — the documented answer is one server per agent, as stdio already gives, not a pretence of isolation.
    2. Lifetime without an MCP session: the stale reaper's trigger becomes a per-debug-session idle policy (same 30-minute default, same env-knob family; activity is any tool call naming the session, so a long wait_for_stop counts), beside explicit close and debuggee exit.

    Forward-compatible: if real auth ever arrives, the authenticated principal becomes the owner key with no schema change.

    So 1.0 does not wait for step 2. Step 1 (the v2 SDK API on the 2025-11-25 wire protocol) starts now as one mechanical PR; the sse command stays in it on @modelcontextprotocol/server-legacy/sse, with its removal as a separate follow-up PR. Step 2 stays gated on a client we serve negotiating 2026-07-28, or on the conformance CLI's 0.2 line going stable. Step 1 adds one deliberate thing beyond the rename: the negotiated protocol version is logged per client, so that trigger is measurable from server logs.

  3. debugmcpdev commented on Nov 3, 2026

    @debugmcpdev
    CollaboratorAuthor

    Step 1 is on main: #1079 merged as 69ac4d8 (the v2 TypeScript SDK on the 2025-11-25 wire protocol, sse kept on @modelcontextprotocol/server-legacy/sse for now). Every CI lane passed on the PR, the conformance suite and http-spec-contract were unchanged, and the bundles grew 10.6–11.7 % (hono, the v2 AJV validator, the legacy SSE package — the last goes with #1078, which is in progress).

    Step 2 (protocol 2026-07-28) stays gated on a client we serve negotiating it. The server now logs [MCP Server] Client initialized with the negotiated protocolVersion and the client name/version on every initialize, so the trigger is measurable from real traffic rather than guessed. The ownership decision above (the handle is the capability) is written into the session-tree design doc's Ownership and lifetime section, which is the input step 2 takes when it starts.

  4. debugmcpdev commented on Nov 3, 2026

    @debugmcpdev
    CollaboratorAuthor

    The sse follow-up (#1078) is up as #1084: the legacy package is gone from the dependency list and the Docker bundle sheds ~99 KB (2,066,956 → 1,968,289), the npx bundle ~209 KB (4,414,619 → 4,205,542). With it merged, the SDK v2 step 1 surface is @modelcontextprotocol/server, node, client and core only.

  5. debugmcpdev commented on Nov 7, 2026

    @debugmcpdev
    CollaboratorAuthor

    Status 2026-11-07: the step-2 gate was mis-instrumented on our side, it has been met for a month, and step 2 starts now.

    The 2026-11-03 comment above gated step 2 on "a client we serve negotiating 2026-07-28, measured from the Client initialized log line". That measurement could never succeed, for three reasons found today:

    1. The log line fires inside the legacy initialize handshake, which a modern-era client only runs as its fallback.
    2. The v2 Server registers the server/discover handler only when its version list holds a modern revision, and only the SDK's serving entries (createMcpHandler, serveStdio) install one. We connect the transports directly, so no mcp-debugger server can answer the probe: over stdio it is a method-not-found; over http it lands in our own 400 "missing Mcp-Session-Id" branch (a discover request has no session id and is not an initialize) before the SDK sees it.
    3. The v2 client's versionNegotiation default is 'legacy'; only a client opting into 'auto' or a pin ever probes.

    What the clients do (checked 2026-11-07):

    Client 2026-07-28 support Default posture Against mcp-debugger today
    Claude Code v2 runtime on SDK 2.x: default from 2.1.274 (feature-flag sessions from 2.1.232); MCP_SDK_GENERATION, MCP_PROTOCOL_NEGOTIATION probes every HTTP and stdio server with server/discover; may remember a stdio server's answer for 7 days legacy. Its per-server logs carry protocolEra: legacy for every mcp-debugger entry on a dev box since 2026-10-09, and "remembered as a legacy peer from an earlier connect (its server/discover probe was answered method-not-found or not at all); skipping the probe"
    Codex full 2026 client (rmcp-client, protocol_mode.rs) feature mcp_2026_07_28, UnderDevelopment, default off legacy
    MCP Inspector 2.14.0 SDK 2.4.0, era probe probes legacy
    VS Code / Copilot none (2025-11-25) — legacy
    pi own client, 2025 initialize only — legacy

    Measured against this server (v2 client 2.4.0, versionNegotiation.mode): 'auto' lands on 2025-11-25 over stdio, http and the dev proxy, with no error and no respawn; { pin: '2026-07-28' } fails cleanly with ERA_NEGOTIATION_FAILED on all three; a raw server/discover POST to /mcp answers HTTP 400 and leaves one Rejecting MCP request: missing session ID warning in the server log per probing connect. Over stdio the probe leaves no trace at all, which is why the gate looked unmet from our side.

    Support windows, from the sources: the spec's versioning page lists 2026-07-28 as the current revision and "2025-11-25 and earlier" as legacy; the TypeScript SDK README says v1 "continues to receive bug fixes and security updates for at least six months after the v2 release (2026-07-27)", with no fixed date; no window is published for the legacy wire era (the twelve-month policy in the changelog is the feature lifecycle). Conformance: latest is still 0.1.16; the 2026-07-28 scenarios exist only in 0.2.0-alpha.12.

    Decision: nothing external gates step 2, and the lead harness asks for it on every connect, so it starts now as a planned multi-PR change rather than on a trigger. The shape is the SDK's own dual-era pattern: the existing sessionful app keeps serving the requests isLegacyRequest classifies as 2025-era; a strict modern handler, createMcpHandler(factory, { legacy: 'reject' }) behind toNodeHandler, serves the rest; serveStdio(factory) on stdio; one factory for both legs. The factory runs per request and a connected Server cannot reconnect, so the core of the work is a per-request Server facade over one shared SessionManager, which is the "one shared server per process" decision above and the session-tree doc's ownership section. The plan follows on this issue.

  6. debugmcpdev commented on Nov 11, 2026

    @debugmcpdev
    CollaboratorAuthor

    Status 2026-11-11: both steps are on main; the issue closes with the 0.27.0 release.

    Step 2 landed as five PRs — #1114 (the SDK Server becomes a per-connection facade over one core), #1115 (one core per http process, the owner-key seam, the opt-in idle policy), #1116 (both eras on stdio), #1117 (both eras on /mcp, the 2026-07-28 conformance lane), #1118 (the dev proxy on both eras) — and #1126 closes out what was left.

    Against this issue's "done when":

    Clause State
    A 2026-07-28 client and a 2025-11-25 client each drive a full debug session over stdio and http Met: stdio-era-negotiation, http-era-negotiation, the two http-spec-contract suites, the http e2e smoke
    Conformance Met: the 0.1.16 lane unchanged at 7/7; the 0.2 alpha's --requirements 2026-07-28 set runs in CI, judged by the suite against a 31-check baseline in which no entry is a divergence of ours
    Docs name the era each transport speaks Met
    A client we serve negotiates the new era Met, measured: Claude Code's own per-server logs record protocolEra: modern, negotiatedProtocolVersion: 2026-07-28 for all fifteen proxy entries on this machine
    The Docker image and the npx bundle pass their smokes Met in the 2025 era; in the 2026 era with #1126, which adds a pinned-2026 case to each and a 2026 leg to the published-artifact canary
    The published tarball speaks both eras to the clients we serve Waits for the release: 0.26.0 is what is published, and it answers a probe as a 2025 server

    So what is left is the 0.27.0 release itself, and its canary and /testpublished passes in both eras. This issue closes with that evidence.

    Follow-ups filed from the work: #1120 (authenticated http mode — the seam is built, the mode is not), #1121 (move the facade from the deprecated low-level Server to McpServer), #1122 (progress notifications), #1123 (isError on failed calls), #1124 (two example scripts that post a non-MCP request), #1125 (CI launches no Java or .NET session). Upstream: our use cases are on modelcontextprotocol/typescript-sdk#2827 (a server-side hook for subscriptions/listen) and modelcontextprotocol/conformance#516 (scenarios that fail a server for capabilities it never declared); the ignored connect() signal the proxy works around is modelcontextprotocol/typescript-sdk#3133.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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