MCP server on SDK v2, serving 2026-07-28 alongside the 2025 era (#185) - #443
Merged
Merged
Conversation
What: @modelcontextprotocol/sdk ^1.29.0 -> @modelcontextprotocol/server +
/core ^2.0.0 (dependencies); @modelcontextprotocol/client ^2.0.0
(devDependency, for scripts/smoke-mcp.mjs). Codemod v1-to-v2 applied to
mcp-server/ (95 rewrites, 0 @mcp-codemod-error markers): imports,
McpError/ErrorCode -> ProtocolError/ProtocolErrorCode,
setRequestHandler(Schema) -> setRequestHandler("tools/list"|"tools/call").
Hand-fixed: import placement the codemod put above the file comment; the
readonly-tuple cast on tools/list (ListToolsResult["tools"]); startStdioServer
now uses serveStdio (the SDK's era-owning stdio entry) with a factory that
calls buildServer() once per connection; buildServer passes
cacheHints: { "tools/list": { ttlMs: 86400000, cacheScope: "public" } }.
The two-handler dispatch and the 22 tools are untouched. 16 test files
swap the import; 4 assertion patterns drop the v1 "MCP error <code>: "
message prefix (flow change, see below). README's spec paragraph,
CHANGELOG. server.json $schema unchanged (see below).
Why: v2 is the `latest` line; the 2026-07-28 era is reachable only on it.
Findings against the INSTALLED packages (node_modules/@modelcontextprotocol):
(a) Low-level `Server` + `setRequestHandler` still exist in v2
(server/dist/index.mjs export list; Server class at
mcp-*.mjs:677, marked @deprecated in favour of McpServer but
supported). Method-name strings replace the Zod request schemas.
(b) The SDK answers server/discover ITSELF (Server._ondiscover,
mcp-*.mjs:1034) — but only when the instance's supported-versions
list contains a modern revision, which a hand-constructed Server
never has (constructor at :733 checks modernProtocolVersions()).
serveStdio installs it on the modern-era instance
(stdio.mjs:378-379 installModernOnlyHandlers). The issue's
"~30 lines" of app-level discover handler is therefore zero lines.
(c) core: LATEST_PROTOCOL_VERSION = "2025-11-25";
SUPPORTED_PROTOCOL_VERSIONS = [2025-11-25, 2025-06-18, 2025-03-26,
2024-11-05, 2024-10-07] (core/dist/auth-*.mjs:4-12). The modern
list is separate: SUPPORTED_MODERN_PROTOCOL_VERSIONS = ["2026-07-28"]
(server/dist/src-*.mjs:551). Same five legacy revisions as v1.30.0.
(d) resultType, ttlMs/cacheScope, _meta serverInfo are stamped by the
SDK's 2026 encode seam (src-*.mjs:2917 ResultTypeSchema.default
("complete"); mcp-*.mjs:806 attachCacheHintFallback). textResult()
needs no change; on 2025-era responses the SDK strips them.
(e) Codemod rewrites: import paths, symbol renames, schema->method
strings, extra.* -> ctx.mcpReq.*; flags ErrorCode split as a warning.
Negotiate-down verdict: the v2 SDK negotiates down on its own;
server-legacy is NOT required (its published description is "Frozen v1
SSE transport and OAuth Authorization Server helpers"; this server is
stdio-only). Evidence, all over real stdio against dist/mcp-server/bin.mjs
(tests/mcp-protocol-2026-07-28.test.mjs, 13 tests):
initialize 2025-11-25 -> protocolVersion 2025-11-25 ok
initialize 2025-06-18 -> 2025-06-18 ok
initialize 2025-03-26 -> 2025-03-26 ok
initialize 2024-11-05 -> 2024-11-05 ok
initialize 2024-10-07 -> 2024-10-07 ok
initialize 2026-07-28 (legacy handshake) -> 2025-11-25 (same as v1)
server/discover (envelope) -> supportedVersions ["2026-07-28"],
resultType complete, ttlMs, cacheScope, _meta serverInfo
tools/list (envelope) -> resultType complete, ttlMs 86400000,
cacheScope public
tools/call (envelope) -> resultType complete, content + structuredContent
discover then initialize -> served 2025 era (probe does not pin)
discover + envelope request then initialize -> -32022 refused
(supported ["2026-07-28"], requested "2025-11-25")
The six legacy tests passed on 1.30.0 before the dependency change (the
non-regression control); the modern-era tests failed there with -32601
(RED), and pass now (GREEN).
Verify items:
- inputSchema under 2020-12: the new test walks every advertised schema
for definitions/dependencies/additionalItems/id, $ref into
#/definitions/, and tuple-form items; none found across 23 tools.
- Error codes: the server emits InvalidRequest -32600, MethodNotFound
-32601, InvalidParams -32602, InternalError -32603 — identical values in
ProtocolErrorCode (src-*.mjs:3520-3523). The v2 renumbering touches
SDK-internal codes only (-32001 timeouts moved to SdkErrorCode;
-32002 ResourceNotFound now receive-only). Nothing this server throws
changed value.
- server.json $schema stays 2025-12-11: .../schemas/2026-07-28/
server.schema.json is HTTP 404 (also every monthly date tried back to
2026-01); every entry on registry.modelcontextprotocol.io and the
2026-07-28 spec checkout's registry docs reference 2025-12-11.
Flow change in four existing assertions: v1 McpError built its message as
`MCP error ${code}: ${message}`; v2 ProtocolError keeps the message bare
(src-*.mjs:362-368). tests/config-problems, publish-path-containment,
skill-name-resolution, stuck-pipeline matched the prefix; each already
asserts error.code separately, so the patterns now match the body only.
No other existing assertion changed.
Counts: before 1276 (main), after 1289 (13 new), 0 failing, run twice.
npm run build: 0 errors. Packed-tarball smoke: 12/12 (9 original via
initialize + 3 via the v2 client's versionNegotiation auto probe).
npm ls: no @modelcontextprotocol/sdk remains; lockfile drops 80 packages
(express, hono, ajv, cors, ...) and adds 3. git diff --check clean; no
local paths in tracked files; npm pack file list unchanged (239 files).
NOT DONE — pre-merge requirement: a real round trip through Claude Code,
Codex and Hermes per CONTRIBUTING "Surface verification". The scripted
harness and the smoke test are not that; an SDK-era swap is exactly the
class of change (#94) that step exists to catch. Do not merge on green
CI alone.
Member
Author
Host round trips (CONTRIBUTING surface verification) — done at
|
| Host | Version | Protocol the host speaks | Result |
|---|---|---|---|
Hermes (native client, mcp 2.0.0) |
Python SDK 2.0.0 | 2026-07-28 | negotiated 2025-11-25 (legacy handshake, as the SDK does); tools/list → 23 tools, carried ttlMs/cacheScope/resultType; tools/call → resultType: complete; a bad cwd came back as a proper MCPError: cwd does not exist: … |
| Claude Code | 2.1.277 | — | claude -p --mcp-config … --strict-mcp-config → # Driving CodeCartographer, is_error: false |
| Codex CLI | 0.154.0 | — | codex exec → mcp: codecarto/codecarto_guide (completed) → # Driving CodeCartographer (the first run under -s read-only discovered the tool but Codex's own headless approval gate refused the call; rerun with approvals bypassed for the read-only tool in the throwaway dir) |
Lifecycle: initialize then stdin closed → process exit 0 after 0.51 s.
Not covered: a 2025-era Hermes (mcp 1.x) client — the negotiate-down tests cover the wire, no such host was on this machine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What: @modelcontextprotocol/sdk ^1.29.0 -> @modelcontextprotocol/server +
/core ^2.0.0 (dependencies); @modelcontextprotocol/client ^2.0.0
(devDependency, for scripts/smoke-mcp.mjs). Codemod v1-to-v2 applied to
mcp-server/ (95 rewrites, 0 @mcp-codemod-error markers): imports,
McpError/ErrorCode -> ProtocolError/ProtocolErrorCode,
setRequestHandler(Schema) -> setRequestHandler("tools/list"|"tools/call").
Hand-fixed: import placement the codemod put above the file comment; the
readonly-tuple cast on tools/list (ListToolsResult["tools"]); startStdioServer
now uses serveStdio (the SDK's era-owning stdio entry) with a factory that
calls buildServer() once per connection; buildServer passes
cacheHints: { "tools/list": { ttlMs: 86400000, cacheScope: "public" } }.
The two-handler dispatch and the 22 tools are untouched. 16 test files
swap the import; 4 assertion patterns drop the v1 "MCP error
: "message prefix (flow change, see below). README's spec paragraph,
CHANGELOG. server.json $schema unchanged (see below).
Why: v2 is the
latestline; the 2026-07-28 era is reachable only on it.Findings against the INSTALLED packages (node_modules/@modelcontextprotocol):
(a) Low-level
Server+setRequestHandlerstill exist in v2(server/dist/index.mjs export list; Server class at
mcp-.mjs:677, marked @deprecated in favour of McpServer but
supported). Method-name strings replace the Zod request schemas.
(b) The SDK answers server/discover ITSELF (Server._ondiscover,
mcp-.mjs:1034) — but only when the instance's supported-versions
list contains a modern revision, which a hand-constructed Server
never has (constructor at :733 checks modernProtocolVersions()).
serveStdio installs it on the modern-era instance
(stdio.mjs:378-379 installModernOnlyHandlers). The issue's
"~30 lines" of app-level discover handler is therefore zero lines.
(c) core: LATEST_PROTOCOL_VERSION = "2025-11-25";
SUPPORTED_PROTOCOL_VERSIONS = [2025-11-25, 2025-06-18, 2025-03-26,
2024-11-05, 2024-10-07] (core/dist/auth-.mjs:4-12). The modern
list is separate: SUPPORTED_MODERN_PROTOCOL_VERSIONS = ["2026-07-28"]
(server/dist/src-.mjs:551). Same five legacy revisions as v1.30.0.
(d) resultType, ttlMs/cacheScope, _meta serverInfo are stamped by the
SDK's 2026 encode seam (src-.mjs:2917 ResultTypeSchema.default
("complete"); mcp-.mjs:806 attachCacheHintFallback). textResult()
needs no change; on 2025-era responses the SDK strips them.
(e) Codemod rewrites: import paths, symbol renames, schema->method
strings, extra.* -> ctx.mcpReq.*; flags ErrorCode split as a warning.
Negotiate-down verdict: the v2 SDK negotiates down on its own;
server-legacy is NOT required (its published description is "Frozen v1
SSE transport and OAuth Authorization Server helpers"; this server is
stdio-only). Evidence, all over real stdio against dist/mcp-server/bin.mjs
(tests/mcp-protocol-2026-07-28.test.mjs, 13 tests):
initialize 2025-11-25 -> protocolVersion 2025-11-25 ok
initialize 2025-06-18 -> 2025-06-18 ok
initialize 2025-03-26 -> 2025-03-26 ok
initialize 2024-11-05 -> 2024-11-05 ok
initialize 2024-10-07 -> 2024-10-07 ok
initialize 2026-07-28 (legacy handshake) -> 2025-11-25 (same as v1)
server/discover (envelope) -> supportedVersions ["2026-07-28"],
resultType complete, ttlMs, cacheScope, _meta serverInfo
tools/list (envelope) -> resultType complete, ttlMs 86400000,
cacheScope public
tools/call (envelope) -> resultType complete, content + structuredContent
discover then initialize -> served 2025 era (probe does not pin)
discover + envelope request then initialize -> -32022 refused
(supported ["2026-07-28"], requested "2025-11-25")
The six legacy tests passed on 1.30.0 before the dependency change (the
non-regression control); the modern-era tests failed there with -32601
(RED), and pass now (GREEN).
Verify items:
for definitions/dependencies/additionalItems/id, $ref into
#/definitions/, and tuple-form items; none found across 23 tools.
-32601, InvalidParams -32602, InternalError -32603 — identical values in
ProtocolErrorCode (src-*.mjs:3520-3523). The v2 renumbering touches
SDK-internal codes only (-32001 timeouts moved to SdkErrorCode;
-32002 ResourceNotFound now receive-only). Nothing this server throws
changed value.
server.schema.json is HTTP 404 (also every monthly date tried back to
2026-01); every entry on registry.modelcontextprotocol.io and the
2026-07-28 spec checkout's registry docs reference 2025-12-11.
Flow change in four existing assertions: v1 McpError built its message as
MCP error ${code}: ${message}; v2 ProtocolError keeps the message bare(src-*.mjs:362-368). tests/config-problems, publish-path-containment,
skill-name-resolution, stuck-pipeline matched the prefix; each already
asserts error.code separately, so the patterns now match the body only.
No other existing assertion changed.
Counts: before 1276 (main), after 1289 (13 new), 0 failing, run twice.
npm run build: 0 errors. Packed-tarball smoke: 12/12 (9 original via
initialize + 3 via the v2 client's versionNegotiation auto probe).
npm ls: no @modelcontextprotocol/sdk remains; lockfile drops 80 packages
(express, hono, ajv, cors, ...) and adds 3. git diff --check clean; no
local paths in tracked files; npm pack file list unchanged (239 files).
NOT DONE — pre-merge requirement: a real round trip through Claude Code,
Codex and Hermes per CONTRIBUTING "Surface verification". The scripted
harness and the smoke test are not that; an SDK-era swap is exactly the
class of change (#94) that step exists to catch. Do not merge on green
CI alone.
Closes #185