Skip to content

Serve the 2026-07-28 MCP revision, on SDK v2 and zod 4 - #10

Merged
chris-cpz merged 1 commit into
mainfrom
feat/mcp-sdk-v2
Sep 15, 2026
Merged

chris-cpz merged 1 commit into
mainfrom
feat/mcp-sdk-v2

Conversation

@chris-cpz

Copy link
Copy Markdown
Contributor

The premise I got wrong

PR #9 deferred the 2026-07-28 revision because the announcement I read was the beta one. It shipped as a stable release line on 2026-07-28 under new package names: @modelcontextprotocol/server 2.0.0 replaces the monolithic @modelcontextprotocol/sdk. So this does it.

What a 2026-07-28 client now gets, on /mcp and /mcp/compact

  • No initialize handshake. Every request stands alone, carrying its protocol version, client info and capabilities in _meta.
  • Mcp-Method and Mcp-Name header routing, so a gateway can dispatch and authorize without parsing the body.
  • server/discover: capabilities, instructions and supported versions in one call.
  • ttlMs and cacheScope on tools/list, prompts/list, resources/list and server/discover. tools/list is private, never public because it is filtered per credential, so a shared cache would hand one key's catalogue to another. 60s matches the scope cache behind it.

Legacy clients are untouched. createMcpHandler runs with legacy: 'stateless', so an initialize on 2025-11-25, 2025-06-18 or older negotiates exactly as before.

zod 3 to 4

Required by the v2 SDK. The generated JSON Schema changed only by being more precise: v1's converter silently dropped constraints on refined strings (emitting {} for trimmed, length-bounded fields), and used format: uuid where v2 emits the explicit pattern. I captured the full v1 catalogue first and diffed all 31 input schemas and 24 output schemas property by property. Nothing lost; place_order and get_bars byte-identical in substance.

Plumbing

Express hands the handler a rebuilt web Request, because express.json() has already drained the socket. The response is streamed back rather than buffered, and cancels the reader if the client hangs up. CORS allows Mcp-Method and Mcp-Name. zod-to-json-schema is gone: zod 4 converts natively, with the same target the SDK uses.

Verification

163 tests, 7 new in tests/modern-protocol.test.ts driving createMcpHandler directly. Live over HTTP on both endpoints and both eras:

/mcp /mcp/compact
legacy (2025-06-18 initialize) 31 tools, 35,962 B 15 tools, 14,347 B
modern (2026-07-28 stateless) 31 tools, 35,962 B, ttlMs 60000, scope private 15 tools, 14,347 B, same

Deploy

Not git-triggered, and --force-new-deployment alone ships nothing: the task definition pins the image by digest. S3 zip, CodeBuild, register a new task-def revision with the new digest, update the service, then confirm the running tasks' imageDigest.

The 2026-07-28 spec shipped as a stable release line under new package
names, not as a beta: @modelcontextprotocol/server 2.0.0 replaces the
monolithic @modelcontextprotocol/sdk. Previous note in this repo that it
was not production ready was reading the beta-era announcement.

WHAT A 2026-07-28 CLIENT NOW GETS, on both /mcp and /mcp/compact:
- no initialize handshake; every request stands alone with its protocol
  version, client info and capabilities in _meta
- Mcp-Method and Mcp-Name header routing, so a gateway dispatches and
  authorizes without parsing the body
- server/discover for capabilities and instructions in one call
- ttlMs and cacheScope on tools/list, prompts/list, resources/list and
  server/discover. tools/list is cacheScope private, never public: it is
  filtered per credential, so a shared cache would hand one key's
  catalogue to another. 60s matches the scope cache behind it.

Legacy clients are untouched: createMcpHandler runs with
legacy: 'stateless', and an initialize on 2025-11-25, 2025-06-18 or older
negotiates exactly as before. Verified live against both eras on both
endpoints: same 31 and 15 tools, same bytes.

zod 3 to 4 came with it. The generated JSON Schema changed only by being
MORE precise: v1's converter silently dropped constraints on refined
strings (emitting {} for trimmed, length-bounded fields) and used
format: uuid where v2 emits the explicit pattern. Diffed all 31 input and
24 output schemas property by property before and after: no property,
required entry, or enum lost anywhere, money-path schemas byte-identical
in substance.

Express feeds the handler a rebuilt web Request because express.json()
has already drained the socket, and the response is streamed back rather
than buffered. CORS now allows Mcp-Method and Mcp-Name.

163 tests, 7 new covering the modern path end to end through
createMcpHandler: no-handshake tools/list, cache hints, server/discover,
compact discover, scope filtering, header-routed tools/call, and the
rejection when the routing header and body disagree.
@chris-cpz
chris-cpz merged commit 5023de3 into main Sep 15, 2026
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