Skip to content

MCP server on SDK v2, serving 2026-07-28 alongside the 2025 era (#185) - #443

Merged
TheAmericanMaker merged 1 commit into
mainfrom
feat/185-mcp-sdk-v2
Sep 22, 2026
Merged

TheAmericanMaker merged 1 commit into
mainfrom
feat/185-mcp-sdk-v2

Conversation

@TheAmericanMaker

Copy link
Copy Markdown
Member

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 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.

Closes #185

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.
@TheAmericanMaker

Copy link
Copy Markdown
Member Author

Host round trips (CONTRIBUTING surface verification) — done at aac88eb

Packed tarball codecartographer-pi-0.26.0.tgz installed into a throwaway npm project; each host was pointed at the installed codecarto-mcp bin and asked to call codecarto_guide with no arguments and reply with the result's first heading.

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.

@TheAmericanMaker
TheAmericanMaker merged commit 72627bc into main Sep 22, 2026
6 checks passed
@TheAmericanMaker
TheAmericanMaker deleted the feat/185-mcp-sdk-v2 branch September 22, 2026 20:13
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