Repository navigation
feat!: move to MCP SDK v2 and Zod 4 (base) - #35
Conversation
Replace @modelcontextprotocol/sdk with @modelcontextprotocol/server and
@modelcontextprotocol/node, and zod ^3.25 with ^4.6.7 (SDK v2 cannot read Zod 3
schemas, so the two move together).
- Transports use NodeStreamableHTTPServerTransport; session handling and the
per-client bundle model are unchanged.
- Tool schemas go through toRegistrableInputSchema(): Zod 4 schemas as is, raw
shapes wrapped with toolception's own z.object(), Zod 3 refused with
E_VALIDATION, JSON Schema kept as an open z.looseObject({}).
- Add multi-client acceptance tests (DYNAMIC, STATIC, permission-based, session
context) and a single-copy dependency test.
- Fix examples, README, intent nodes and changelog; vite externals follow the
new packages; npm audit fix clears the zod high advisory.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook
TS review:
|
| # | Sev | Location | Finding |
|---|---|---|---|
| 1 | MEDIUM | package.json dependencies, README.md:82-86 |
@modelcontextprotocol/server and zod are regular dependencies although consumers must share the same copy; only this repo's lockfile is guarded against duplicates. Consider peerDependencies. |
| 2 | LOW | tests/smoke-e2e/permission-config-server-demo.ts:87, permission-header-server-demo.ts:89 |
Codemod left the ; inside the trailing comment (}) // STATIC mode, no listChanged;); works only through ASI. |
| 3 | LOW | src/http/FastifyTransport.ts:403, src/permissions/PermissionAwareFastifyTransport.ts:184 |
TSDoc quotes the old "Already connected" message; v2 throws SdkError AlreadyConnected with different wording. |
| 4 | LOW | src/http/http.utils.ts:300, CHANGELOG.md |
VALIDATION_ERROR details now carries Zod 4 issue objects (different codes/messages); not listed under Breaking. |
| 5 | LOW | src/core/DynamicToolManager.ts:398,406 |
isZod4Type / isZod3Type have @param/@returns but no summary sentence. |
| 6 | LOW | tests/e2e/multiClient.e2e.test.ts:67 |
settle() is a fixed 100 ms sleep; poll for the positive notification instead. |
| 7 | LOW | tests/dependencies.test.ts:25 |
Asserts a single copy of the SDK-internal @modelcontextprotocol/core; brittle if the SDK reshuffles packages. Reads the lockfile, not the installed tree. |
| 8 | LOW | tests/dynamicToolManager.test.ts (Zod 3 refusal) |
"Registers nothing" uses a one-tool toolset, so rollback of already registered tools is not exercised. |
{
"recommendation": "COMMENT",
"summary": "The SDK v2 and Zod 4 migration is correct and well documented, and the multi-client e2e tests do detect state leakage; only MEDIUM and LOW issues remain.",
"findings": [
{ "severity": "MEDIUM", "file": "package.json", "line": null, "issue": "@modelcontextprotocol/server and zod are regular dependencies; consider peerDependencies." },
{ "severity": "LOW", "file": "tests/smoke-e2e/permission-config-server-demo.ts", "line": 87, "issue": "Statement terminator left inside trailing comment (also permission-header-server-demo.ts:89)." },
{ "severity": "LOW", "file": "src/http/FastifyTransport.ts", "line": 403, "issue": "TSDoc quotes the old 'Already connected' error text (also PermissionAwareFastifyTransport.ts:184)." },
{ "severity": "LOW", "file": "src/http/http.utils.ts", "line": 300, "issue": "VALIDATION_ERROR details shape changes under Zod 4; not in changelog." },
{ "severity": "LOW", "file": "src/core/DynamicToolManager.ts", "line": 398, "issue": "isZod4Type/isZod3Type lack a TSDoc summary sentence." },
{ "severity": "LOW", "file": "tests/e2e/multiClient.e2e.test.ts", "line": 67, "issue": "Fixed 100 ms sleep in the list_changed isolation test." },
{ "severity": "LOW", "file": "tests/dependencies.test.ts", "line": 25, "issue": "Single-copy assertion on SDK-internal @modelcontextprotocol/core is brittle." },
{ "severity": "LOW", "file": "tests/dynamicToolManager.test.ts", "line": null, "issue": "Zod 3 refusal test does not exercise rollback of already registered tools." }
]
}Generated by Claude Code
- demos: statement terminator before the trailing comment - TSDoc: name the AlreadyConnected error instead of the v1 message - DynamicToolManager: summary sentences on the Zod type guards - changelog: VALIDATION_ERROR details carry Zod 4 issues - tests: poll for list_changed instead of a fixed sleep, drop the SDK-internal core package from the single-copy check, and cover rollback of an earlier tool in the Zod 3 refusal test Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook
Fix mapping for the TS review (round 1)Fixes are in ac12cac. Numbers refer to the findings table in the review comment.
Re-run: typecheck ✔, 411 tests ✔ (three consecutive runs, Node 26). Re-review follows. Generated by Claude Code |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook
TS review:
|
| Sev | Location | Finding |
|---|---|---|
| LOW | tests/e2e/multiClient.e2e.test.ts, list_changed isolation test |
Removing the initial settle() also removed the implicit wait for the client's standalone SSE stream to open; a notification sent earlier would be dropped and waitFor would time out (fail-loud, unlikely on localhost). Optional: await a round trip on A first. |
Resolution of the LOW (231a26c, test-only, one block): both clients now do a listTools round trip before the counts are taken, so their SSE streams are open. The multi-client file passed three times in a row, and the full suite passes (411 tests, Node 26).
{
"recommendation": "APPROVE",
"summary": "All 8 round-1 resolutions are correct and complete; the delta adds no CRITICAL/HIGH issues, only a LOW SSE-timing note in a test (since addressed in 231a26c).",
"findings": [
{ "severity": "LOW", "file": "tests/e2e/multiClient.e2e.test.ts", "line": null, "issue": "Removing the initial settle() drops the implicit wait for the standalone SSE stream to open before enable_toolset fires." }
]
}Generated by Claude Code
…-dts chore(deps): update vite-plugin-dts to ^5.1.2 (stacked on #35)
…-node ci: run CI and release on Node 26 (stacked on #35)
Requested by Ben · project thread
Stack and merge order
This is the base PR (B1) of a stack. Merge order:
mainmainafter this merges): T1 chore(deps): update vite-plugin-dts to ^5.1.2 (stacked on #35) #36vite-plugin-dts5 (sprintf/minimatch advisory), T2 ci: run CI and release on Node 26 (stacked on #35) #37 CI/release on Node 26 (vitest 5), T3 JSON Schema tools viafromJsonSchema()(separate thread)v0.9.0Nothing here is tagged or published.
What changed
@modelcontextprotocol/sdk→@modelcontextprotocol/server^2.4.0 +@modelcontextprotocol/node^2.1.2;zod^3.25 → ^4.6.7;@modelcontextprotocol/clientis a dev dependency for tests. SDK v2 cannot read Zod 3 schemas, so the two cannot be split.@modelcontextprotocol/codemodv1-to-v2, then cleared its one@mcp-codemod-errormarker.FastifyTransportandPermissionAwareFastifyTransportuseNodeStreamableHTTPServerTransport. Session handling, the readiness gate,originValidation.tsand the per-client bundle model are unchanged (2025-era session path, nocreateMcpHandler).DynamicToolManager.toRegistrableInputSchema(): Zod 4 schema → as is; raw shape of Zod 4 values (incl.{}) → wrapped with toolception's ownz.object(); Zod 3 schema →E_VALIDATIONnaming the tool, nothing registered; anything else → JSON Schema passthrough, nowz.looseObject({})(T3 changes this).http.utils.ts:ZodError.errors→.issues.vite.config.tsexternals follow the new packages (otherwise the SDK would be bundled intodist).capabilitiesnow passed as the 2ndMcpServerargument (the old position is a type error in v2).core,http,typesnodes) and CHANGELOG[Unreleased](breaking changes and migration).Must-have: multiple clients, each with its own toolset state
New
tests/e2e/multiClient.e2e.test.ts(8 tests) covers, concurrently connected clients:tools/list_changedonly reaches the client whose state changedMcpServercreated per client idtests/dependencies.test.ts: exactly one copy ofzodand@modelcontextprotocol/serverin the lockfileBehaviour changes to know about
isError: true(SDK v2). Three existing tests re-baselined.tools/list_changed; enable of one tool sends 2 (SDK + toolception). Checked on v2 with and without declared capabilities (listChangedis advertised either way).VALIDATION_ERRORdetailsof custom endpoints now carry Zod 4 issue objects (listed in the changelog).Checks (Node 26.13.0)
npm run typecheck✔,npm run build✔ (dist imports@modelcontextprotocol/server/node, not bundled),npm run test:run✔ 411 tests (was 395)Advisories
zod,body-parser). Afternpm audit fixon the new tree: 4 moderate, all the dev-onlyminimatchchain viavite-plugin-dts4 (cleared by T1, chore(deps): update vite-plugin-dts to ^5.1.2 (stacked on #35) #36).npm audit --audit-level=criticalpasses.zodhigh (GHSA-mrhx-c7xc-m3w5) cleared by 4.6.7;body-parser,qs,ip-addressleft the tree with the v1 SDK's Express deps;fastify5.12.8,fast-uri3.1.9.Review
TS review (
pipeline-ts-reviewer) round 1 COMMENT, round 2 APPROVE; see the PR comments.peerDependenciesfor@modelcontextprotocol/serverandzodis deliberately not part of this PR (todo e3).🤖 Generated with Claude Code
https://claude.ai/code/session_01VmUXGSYMuTcM7fquxzRook