Repository navigation
Conversation
Replaces the single `@modelcontextprotocol/sdk@^1.26.0` dependency with the
split v2 packages (`@modelcontextprotocol/{server,node,server-legacy}@^2.0.0`)
and bumps zod to `^4.2.0`, which v2 requires.
Applied by `npx @modelcontextprotocol/codemod@2.0.0 v1-to-v2 .`
(54 changes across 29 files):
- Rewrote every `@modelcontextprotocol/sdk/...` import to its v2 package.
- `StreamableHTTPServerTransport` -> `NodeStreamableHTTPServerTransport`
from `@modelcontextprotocol/node` (this is a Node/express host).
- `SSEServerTransport` -> `@modelcontextprotocol/server-legacy/sse`.
- Schema-first `setRequestHandler(XRequestSchema, ...)` -> method strings
(`resources/list`, `resources/subscribe`, `resources/unsubscribe`).
- `extra` -> `ctx`: `extra._meta` -> `ctx.mcpReq._meta`,
`extra.requestId` -> `ctx.mcpReq.id`, `extra.sendRequest` -> `ctx.mcpReq.send`.
- Wrapped the raw `argsSchema` / `inputSchema` shapes in `z.object()`
(complexPrompt, resourcePrompt, elicitationAllOptional).
- Rewrote package.json dependencies.
Done manually:
- Vendored `InMemoryEventStore` into `inMemoryEventStore.ts`. streamableHttp.ts
imported it from `@modelcontextprotocol/sdk/examples/shared/...`, which was
never a public export and does not exist in v2. Uses `[...].sort()` rather
than the SDK example's ES2023 `.toSorted()`, since this package targets ES2022.
- Bumped zod `^3.23.8` -> `^4.2.0`. The codemod warns that a zod-3 range cannot
satisfy v2's floor; it installs and typechecks fine and then fails at runtime
on the first `tools/list`. No zod-3-only type API (`z.ZodTypeDef`, old
`z.ZodType` generics) was in use, so no schema fallout.
- Switched `ctx.mcpReq.send(..., ElicitResultSchema, opts)` to
`ctx.mcpReq.elicitInput(params, opts)` in elicitationAllOptional.ts. v2
resolves spec-method result types from the method name, so the schema argument
is dropped; `elicitInput` is the intended handler-context helper. This removed
the last `@modelcontextprotocol/core` import, so that package is not declared.
- Added `.meta({ id: "Person" })` to jsonRefTest's PersonSchema. zod 4 defaults
to `reused: "inline"`, so this fixture -- whose entire purpose is exercising
`$ref` -- stopped emitting one. It now emits `$defs`/`$ref` again.
- Bumped `engines.node` `>=18` -> `>=20`; all v2 packages declare `node >=20`.
- tsconfig `module`/`moduleResolution` `ESNext`/`node` -> `NodeNext`/`NodeNext`.
Not strictly required (the v2 packages ship a `typesVersions` node10 fallback,
so legacy resolution still typechecks), but node10 is deprecated and NodeNext
models what Node actually does for this `"type": "module"` package.
- Cleared the 13 `@mcp-codemod-error` "could not verify inputSchema is a schema
object" markers after confirming each inputSchema is a real `z.object()`.
- Reindented the two `argsSchema` blocks the codemod wrapped (it does not format).
Verified:
- `npx tsc --noEmit` — clean (repo has no typecheck script).
- `npm run build` — clean from a removed dist/.
- No test suite exists in this repo, so none was run.
- stdio smoke test over `dist/index.js`: initialize, tools/list (13 tools, all
with non-empty inputSchema), resources/list (+ cursor page 2),
resources/templates/list, prompts/list, all 13 tools/call, 5 resources/read,
subscribe/unsubscribe, all 3 prompts/get. longRunningTask emitted both
notifications/progress frames. An intentionally invalid strictTypeValidation
call was rejected as expected.
- stdio run with a client advertising `elicitation`: 14 tools listed and a full
elicitation/create round trip returned the submitted values.
- Streamable HTTP on :3102 via curl: initialize (session id issued, eventStore
event ids present), tools/list, tools/call, resources/list, prompts/get,
DELETE session -> 200.
- SSE on :3101 via the frozen server-legacy transport: endpoint event,
initialize, tools/list (13), tools/call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both are fallout from zod 4, not from the SDK v2 API surface, and both were
invisible to typecheck -- they only show up by diffing the advertised tool
schemas against the v1 baseline.
formatData: `data` became required
zod 3 treated a bare `z.any()` as optional; zod 4 treats it as required. The
advertised schema gained `"required": ["data"]` and a call that succeeded on v1
(`{"format":"json"}` -> "Data formatted as json:\n\nundefined") started coming
back as isError. Restored with an explicit `.optional()`.
add / complexOrder: reused subschemas lost their $refs
zod 4's default `reused: "inline"` policy inlines a subschema at every use site
instead of hoisting it. `add.b` was `{"$ref":"#/properties/a"}` and became an
inlined copy; `complexOrder.shippingAddress` was `{"$ref":"#/properties/billingAddress"}`
and became a duplicated ~20-line literal. `jsonRefTest` already got the
`.meta({ id })` treatment during the migration; `add` and `complexOrder` use the
same reuse pattern and were missed. In a fixture server whose purpose is
exercising $ref rendering, silently losing $refs defeats the fixture.
Verified against live tools/list on the built output: formatData has no `required`
array and the no-data call succeeds with the v1 text; add ($defs.AddOperand),
complexOrder ($defs.Address) and jsonRefTest ($defs.Person) all emit $ref again.
tsc --noEmit and npm run build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up commit: two zod-4 regressions found in review, now fixedAn independent review caught two behavior changes that typecheck could not see — both zod 3 → 4 fallout rather than SDK v2 API changes. Fixed in the follow-up commit on this branch:
Verified on live Not fixed — needs a decisionConcurrent Streamable HTTP sessions now silently serve the wrong tool list. v2's On v1 this failed loudly (500 for every session after the first). On v2 it fails silently, which is worse. The structural fix is v2's Other disclosures
|
Addendum: the multi-session issue is worse than described aboveA second review pass sharpened the shared-
Same root cause, same fix ( |
|
Closing unmerged — this repository is being archived rather than migrated to MCP TypeScript SDK v2. The work itself is complete and was reviewed twice (final verdict: merge-with-nits, no blocking findings, no type-safety erosion). It is preserved on the
One pre-existing defect was documented here but never fixed, and it outlives the migration — worth knowing if this code is revived: the single module-scope |
Targets MCP TypeScript SDK v1, which is superseded by the v2 packages. The completed v1 -> v2 migration is preserved on chore/migrate-mcp-sdk-v2 and in PR #58 (closed unmerged) for anyone forking this. Also records the unfixed shared-McpServer defect: one module-scope server across all Streamable HTTP sessions, so _clientCapabilities is overwritten per session and an elicitation-gated tool call can hang for the full request timeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migrates this server from MCP TypeScript SDK v1 to the split v2 packages.
Package swap
@modelcontextprotocol/sdk^1.26.0@modelcontextprotocol/server^2.0.0@modelcontextprotocol/node^2.0.0(Streamable HTTP over express)@modelcontextprotocol/server-legacy^2.0.0(frozen SSE transport)zod^3.23.8zod^4.2.0engines.node>=18.0.0>=20.0.0@modelcontextprotocol/coreis not declared — the only symbol that would have needed it (ElicitResultSchema) went away when the elicitation call moved toctx.mcpReq.elicitInput()(see below).Mechanical rewrites (codemod)
npx @modelcontextprotocol/codemod@2.0.0 v1-to-v2 .— 54 changes across 29 files.sdk/server/mcp.js→@modelcontextprotocol/server(McpServer,ResourceTemplate)sdk/types.js→@modelcontextprotocol/server(CallToolResult,Resource)sdk/server/stdio.js→@modelcontextprotocol/server/stdiostdio.tsStreamableHTTPServerTransport→NodeStreamableHTTPServerTransportfrom@modelcontextprotocol/nodestreamableHttp.tsSSEServerTransport→@modelcontextprotocol/server-legacy/ssesse.tssetRequestHandler(ListResourcesRequestSchema, …)→setRequestHandler('resources/list', …)resources/index.tssetRequestHandler(Subscribe/UnsubscribeRequestSchema, …)→'resources/subscribe'/'resources/unsubscribe'resources/subscriptions.tsextra→ctx;extra._meta→ctx.mcpReq._meta;extra.requestId→ctx.mcpReq.idtools/longRunningTask.tsextra.sendRequest→ctx.mcpReq.sendtools/elicitationAllOptional.tsz.object()(argsSchema,inputSchema)prompts/complexPrompt.ts,prompts/resourcePrompt.ts,tools/elicitationAllOptional.tspackage.jsondependency rewritepackage.jsonThe
registerTool/registerResource/registerPromptcall sites needed no shape change — this repo was already on the non-variadicregister*API, andregisterResourcealready passed a metadata argument everywhere.Manual changes, and why each was needed
1. Vendored
InMemoryEventStore—inMemoryEventStore.ts(new)streamableHttp.tsimported@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js. That was never a public export and has no v2 equivalent, so the codemod could only flag it. The implementation is now vendored from the SDK'sexamples/shared/src/inMemoryEventStore.ts, importingEventStore/JSONRPCMessagefrom@modelcontextprotocol/server.One deliberate deviation: the SDK example uses
Array.prototype.toSorted()(ES2023). This package targets ES2022, so the vendored copy uses[...this.events.entries()].sort(...).2. zod
^3.23.8→^4.2.0v2 requires zod ≥ 4.2.0 (it self-converts schemas via
~standard.jsonSchema). This is the failure mode that typechecking does not catch: a zod-3 range installs cleanly, compiles cleanly, the server starts and connects normally, and then the firsttools/listanswers with an error pointing atfromJsonSchema(). Caught only by the runtime smoke test.No zod-3-only type API (
z.ZodTypeDef, the oldz.ZodTypegenerics) was in use, so there was no consumer-side zod-4 fallout to fix.3.
ctx.mcpReq.send(…, ElicitResultSchema, opts)→ctx.mcpReq.elicitInput(params, opts)—tools/elicitationAllOptional.tsThe codemod mapped
extra.sendRequest→ctx.mcpReq.sendbut leaves the result-schema argument alone (it only drops it fromclient.request()/client.callTool()). For spec methods v2 resolves the result type from the method name, so the schema argument has to go.elicitInputis the purpose-built handler-context helper forelicitation/create, so the call now reads as the params object directly. This removed the file's last@modelcontextprotocol/coreimport.4.
.meta({ id: "Person" })onjsonRefTest'sPersonSchemaThis one is a behavior restoration, please review it.
jsonRefTestexists specifically to exercise$refin an advertisedinputSchema. Under v1 + zod 3 it emitted:zod 4 defaults to
reused: "inline", so after the bump both branches were inlined and the fixture stopped testing$refat all. Tagging the shared subschema with anidmakes zod hoist it, which restores$refin the cleaner$defsform:If you'd rather the fixture reflect stock zod-4 output, drop the
.meta()call — everything else still passes.5.
engines.node>=18.0.0→>=20.0.0All three v2 packages declare
"engines": { "node": ">=20" }. Leaving>=18would advertise support this package no longer has.6. tsconfig
module/moduleResolution:ESNext/node→NodeNext/NodeNextCorrecting an assumption: I expected v2's export maps to fail under the legacy
node(node10) resolver. They do not — the v2 packages ship atypesVersionsfallback ({"*":{"sse":["dist/sse/index.d.mts"], "…": "…"}}) precisely so node10 still finds subpath types. I verifiedmoduleResolution: node10typechecks this repo cleanly.So this change is not required by the migration. I made it anyway because node10 is deprecated in TS 5.x, and
NodeNextmodels what Node actually does at runtime for a"type": "module"package instead of relying on a compat shim. It's an isolated two-line change if you'd prefer a more minimal diff. Emit is unchanged (dist/*.js+dist/*.d.ts, matching the publishedmain/types).7. Cleared 13
@mcp-codemod-errormarkersThe codemod could not statically prove that
inputSchema:referenced a Standard Schema object in 13 tool files (each passes a module-levelconst). I inspected all 13 — every one is a realz.object(...)— and removed the markers. No marker remains anywhere in the tree.8. Reindented two wrapped
argsSchemablocksThe codemod does not reformat, and this repo has no prettier/eslint config, so this was done by hand and touches only the two blocks it wrapped.
Behavioral changes reviewers should know about
Advertised tool schemas change shape. Expected and spec-conformant, but this repo exists to be a fixture server, so the diffs are worth knowing. Measured by building v1 and v2 side by side and diffing real
tools/listoutput:$schema:http://json-schema.org/draft-07/schema#→https://json-schema.org/draft/2020-12/schema.additionalProperties: falseis no longer emitted on any object.userFilterTest.is_activewent from"type": ["boolean","null"]to ananyOfofboolean/null. (Note: this tool's description claims "$ref to $defs", but v1 never emitted a$refhere either — pre-existing, not a regression.)strictTypeValidationgained richer constraints from zod 4:format: "email"plus apattern,minimum/maximumbounds onintegerField,exclusiveMinimum: 0for.positive().jsonRefTestmoved from a positional$ref(#/properties/husband) to a named one (#/$defs/Person) — see manual change 4.Anything pinning this server's advertised tool list needs re-baselining.
Unknown / unregistered tool calls now reject. v2 answers
ProtocolError(-32602 InvalidParams)where v1 resolvedCallToolResult{ isError: true }. Visible here viaelicitationAllOptional, which only registers when the client advertises theelicitationcapability — a client without it now gets a JSON-RPC error rather than anisErrorresult.Capabilities are advertised with
listChanged: true. v2'sMcpServereagerly installs capability handlers, soinitializenow returns{"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true},"prompts":{"listChanged":true}}.SSE stays on a frozen copy.
SSEServerTransportis removed from v2. Rather than delete a working entry point,sse.tsandnpm run start:ssenow use@modelcontextprotocol/server-legacy/sse— a frozen v1 copy, and a supported interim choice. It installs with a deprecation notice from npm and will not receive new features; the long-term path is Streamable HTTP, which this repo already exposes viastart:streamableHttp.This is a published library (
mcp-maintainer-toolkit@0.1.5) that exportscreateServer. The returnedMcpServeris now a v2 type, and v1/v2 objects do not interoperate (instanceofand nominal types do not cross the boundary). Any consumer importingcreateServermust migrate to v2 in the same step, so this warrants a semver-major release.Verification
Everything below was actually run against this branch.
npx tsc --noEmittypecheckscript in repo)rm -rf dist && npm run buildtestscriptgrep -rn "@modelcontextprotocol/sdk" . --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.gitinMemoryEventStore.tsgrep -rn '@mcp-codemod-error' .npm ls --depth=0,npm ls honostdio — 31 JSON-RPC frames piped into
node dist/index.js:initialize,tools/list,resources/list,resources/templates/list(4),prompts/list(3) — all OK.tools/listreturned all 13 tools, every one with a non-emptyinputSchema— this is the check that proves the zod bump landed; a missed bump surfaces here as afromJsonSchemaerror.tools/callagainst all 13 tools succeeded, including every adversarial fixture (complexOrderwith nested address/items,strictTypeValidation,unionTypeTest,enumDropdownTest,jsonRefTest,userFilterTest,longDescriptionTest).strictTypeValidationcall was rejected withisError: trueand an input-validation message, confirming the 2020-12 validator is wired up.longRunningTaskemitted bothnotifications/progressframes, confirmingctx.mcpReq._meta.progressToken+relatedRequestId: ctx.mcpReq.id.resources/readon all 5 shapes (static text, static blob,test://users{?…},test://api/v1/posts/{id}{?…},test://search{?q});resources/listwithcursorreturned page 2, confirming theresources/listoverride.resources/subscribe+resources/unsubscribeboth returned{}.prompts/geton all 3 prompts, including the image and embedded-resource content blocks.stdio with
elicitationadvertised — a driver client that declares the capability and answers the server's request: 14 tools listed (the conditional tool registers), fullelicitation/createround trip, submitted values echoed back correctly.Streamable HTTP —
PORT=3102 node dist/streamableHttp.js, driven with curl (Content-Type: application/json,Accept: application/json, text/event-stream):initializeissued a session id and SSE frames carried event ids (confirming the vendoredInMemoryEventStoreis being written to), thennotifications/initialized→ 202,tools/list→ 13 tools,tools/call add→ 42,resources/list→ 10 + cursor,prompts/get complex_prompt, andDELETE /mcp→ 200.SSE —
PORT=3101 node dist/sse.js, full round trip over the frozen transport:GET /ssereturned theendpointevent, theninitialize,tools/list(13), andtools/call echoall resolved over the stream.🤖 Generated with Claude Code