Skip to content

v2: codemod iterations, canonical zod schema exports from sdk-shared#2354

Merged
felixweinberger merged 25 commits into
mainfrom
feature/codemod-iterations-3
Jun 25, 2026
Merged

v2: codemod iterations, canonical zod schema exports from sdk-shared#2354
felixweinberger merged 25 commits into
mainfrom
feature/codemod-iterations-3

Conversation

@KKonstantinov

@KKonstantinov KKonstantinov commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Introduce @modelcontextprotocol/core as the public home for the MCP specification and OAuth/OpenID Zod schemas, and teach the v1→v2 codemod to migrate schema usage to it — plus several codemod accuracy and correctness fixes surfaced by running the migration against real-world v1 repos and by code review. This PR also renames the private internal barrel @modelcontextprotocol/corecore-internal, freeing the clean core name for the public schema package (published in alpha as @modelcontextprotocol/sdk-shared).

Motivation and Context

In v2, @modelcontextprotocol/server and @modelcontextprotocol/client deliberately expose a Zod-free public surface. That left the raw spec Zod schemas (CallToolResultSchema, ListToolsResultSchema, …) — which v1 users imported from @modelcontextprotocol/sdk/types.js — with no public home. Code that did runtime validation like CallToolResultSchema.safeParse(value) had nowhere to import the schema from after migrating, and the codemod's previous workaround rewrote those calls into the Standard-Schema form (specTypeSchemas.X['~standard'].validate(...)), which changed user code more than necessary. The same gap applied to the OAuth/OpenID schemas (OAuthTokensSchema, OAuthMetadataSchema, …) that v1 exposed from @modelcontextprotocol/sdk/shared/auth.js.

This PR closes that gap and tightens the migration:

  • New package @modelcontextprotocol/core — the canonical, public home for the spec Zod schemas and the OAuth/OpenID auth schemas (OAuthTokensSchema, OAuthMetadataSchema, IdJagTokenExchangeResponseSchema, …). It bundles the SDK's internal schema definitions and re-exports only the *Schema Zod constants, so <Name>Schema.parse(value) / .safeParse(value) keep working unchanged — the migration becomes a one-line import-path swap. The auth group mirrors core-internal's own spec-vs-auth split and is kept in sync with core-internal's authSchemas registry by a drift-guard test. Spec types, error classes, enums, and guards continue to live on server/client. The package is runtime-neutral with zod as its only runtime dependency.
  • Package rename (corecore-internal, sdk-sharedcore). The private, unpublished internal barrel formerly named @modelcontextprotocol/core becomes @modelcontextprotocol/core-internal, freeing the clean core name for the public schema package above (published in alpha as @modelcontextprotocol/sdk-shared). A repo-wide, behavior-preserving rename of two packages and their directories — client/server/middleware now consume the internal barrel under its new name. Recorded for the changelog by the rename-sdk-shared-to-core changeset.
  • Codemod: route schema imports to core. *Schema symbols imported from @modelcontextprotocol/sdk/types.js are now routed to @modelcontextprotocol/core (a mixed import { CallToolResult, CallToolResultSchema } is split — the type resolves by context, the schema to core). The v1 OAuth/OpenID *Schema constants from @modelcontextprotocol/sdk/shared/auth.js are routed the same way (the auth types keep resolving to client/server by context). The old specSchemaAccess transform and its generated schema-map are removed in favor of this behavior-preserving import swap.
  • Codemod accuracy fixes (found by running the batch test against firebase/firebase-tools):
    • Match extensionless SDK subpath specifiers (e.g. @modelcontextprotocol/sdk/types as well as .../types.js).
    • Infer client/server project type from source for v1 projects. A project mid-migration still declares the single v1 @modelcontextprotocol/sdk dependency, so detection from package.json came back "unknown" and every file importing only shared symbols defaulted to the server package with an action-required warning. The codemod now scans quoted @modelcontextprotocol/sdk/client/… and …/server/… import specifiers to infer the type, routing shared symbols to the installed package and replacing the spurious warnings with — at most — an info note for genuinely ambiguous "both" projects.
    • Map the task request/notification handlers (tasks/get, tasks/result, tasks/list, tasks/cancel, notifications/tasks/status) to their v2 method strings so task handlers migrate to the 2-arg form instead of falling through to manual migration.
    • Flag namespace-imported schema usage (import * as t … t.CallToolResultSchema.parse()), which can't be split automatically, with an actionable diagnostic pointing at core. The diagnostic now reports the v2 schema name (after renames) so the suggested import is copy-pasteable.
  • Codemod correctness fixes (code review):
    • Guard the schema-param removal so a literal undefined second argument is only dropped in files that actually import the MCP SDK (avoids touching unrelated 3-arg calls).
    • Capture a removed import's leading file-header/JSDoc comment as exact source bytes (not a \n-rejoin of comment ranges), so CRLF files and blank-line-separated header blocks no longer duplicate the header when the first import is rewritten in place.
  • @modelcontextprotocol/coreclient/server consistency. Registering the auth schemas as the canonical core group is keyed off core-internal's authSchemas. The one v2-only addition, IdJagTokenExchangeResponse, is now a public spec type: its TypeScript type is exported from client/server, 'IdJagTokenExchangeResponse' joins SpecTypeName, and isSpecType.IdJagTokenExchangeResponse / specTypeSchemas.IdJagTokenExchangeResponse validate it — matching the already-public OAuth/OpenID spec types. This additive (non-breaking) surface is covered by a client/server changeset.
  • Migration docs updated to describe the schema-validation path (import from core; .parse() unchanged), with the Zod-free isSpecType / specTypeSchemas as an alternative; OAuth/OpenID schemas are noted as also moving to core.

How Has This Been Tested?

  • Unit tests: the codemod suite passes (spec + OAuth/OpenID import routing, schema→method mapping, project-type inference incl. comment/extensionless edge cases, namespace-import diagnostics, leading-comment preservation incl. CRLF/blank-line headers), plus core tests (schema round-trip + drift guards asserting every spec and auth schema is re-exported). Three drift guards keep the auth set aligned: core's auth exports equal core-internal's authSchemas, and the codemod's AUTH_SCHEMA_NAMES (the v1 set) is a subset of core's auth exports.
  • Full monorepo gate: build:all, check:all (typecheck + lint + docs) all green. The core bundle was verified runtime-neutral — zod external, no Node built-ins, no Protocol/transport/validator code pulled in, and the .d.ts exposes the schemas as real values (not type-only aliases).
  • End-to-end on a real repo: the codemod batch test against firebase/firebase-tools — baseline typecheck 0 errors, post-codemod typecheck 0 errors (0 introduced). After the accuracy fixes, the codemod's diagnostics on that repo dropped to just the two genuinely-actionable manual-verification notes (SSE-transport deprecation, ErrorCode split). Spot-checked the migrated output: schemas import from @modelcontextprotocol/core and .parse() calls are preserved verbatim.

Breaking Changes

None for existing v2 consumers. @modelcontextprotocol/core is a new, additive package. server and client gain one small additive (non-breaking) public-API surface — the v2-only IdJagTokenExchangeResponse spec type (type export + SpecTypeName member + isSpecType/specTypeSchemas entry), covered by a client/server changeset; the private core-internal package registers it. The middleware packages are unchanged. The package rename is invisible to stable consumers: the internal barrel (corecore-internal) was always private and unpublished, and the only public-name change — the alpha @modelcontextprotocol/sdk-shared@modelcontextprotocol/core — affects only those who installed the alpha schema package, who update with a one-line import swap (sdk-sharedcore) that the codemod applies automatically (rename-sdk-shared-to-core changeset). The schema-import-location change core supports is part of the existing v1→v2 migration (not new breakage) and is applied automatically by the codemod and documented in the migration guide.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

  • Why core bundles core-internal rather than owning the schemas. An alternative was prototyped where core owned the schemas and server/client referenced it externally to avoid duplicating the schema bytes. It worked but was invasive (it touched core-internal/server/client/middleware build configs and required guarding against a rolldown value-export degradation). This PR takes the lower-footprint approach: core bundles the (private) core-internal schema module via a narrowed alias and re-exports only the *Schema values, leaving server/client's build and schema-bundling untouched (their only public-API delta is the additive IdJagTokenExchangeResponse spec type noted above). The trade-off is that server/client keep their own internally-bundled schema copies (not part of their public API) — an accepted cost for a much smaller, lower-risk change.
  • Project-type inference / task-handler mapping adopt the approach from PR Improve v1→v2 migration ergonomics: Zod-compatible specTypeSchemas + codemod fixes #2277 (thanks @felixweinberger). The project-type inference here refines it to match quoted import specifiers rather than whole-file substrings, so paths mentioned in comments/prose don't cause false positives, and it also recognizes extensionless/bare subpaths.
  • The codemod's "could not determine project type" warning is now reserved for genuinely ambiguous cases; well-formed v1 projects migrate without it.

KKonstantinov and others added 6 commits June 23, 2026 08:56
  IMPORT_MAP was looked up by exact key, so extensionless specifiers like
  @modelcontextprotocol/sdk/types (vs .../types.js) fell through to an
  'Unknown SDK import path' diagnostic and were left unmigrated. Add a
  shared lookupImportMapping() that tolerates .js/.mjs/.cjs extension
  variance, and use it for import, re-export, and mock-path resolution.

  Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  Claude-Session: https://claude.ai/code/session_016f6h88mdVxLUdx1cNT96pW
Shared protocol types/constants resolve to either @modelcontextprotocol/client or /server. The
  codemod read that choice from package.json, but a project mid-migration still declares the single v1
  @modelcontextprotocol/sdk dependency, so the project type came back 'unknown' and every file importing
  only shared symbols defaulted to server with an action-required warning.

Infer from source instead: when the v2 split deps are absent, scan for quoted
  @modelcontextprotocol/sdk/client/ and .../server/ import specifiers (both -> 'both', one -> that side,
  neither -> 'unknown'). Matching quoted specifiers rather than bare substrings ignores comments/prose and
  catches extensionless/bare subpaths. For a 'both' project, shared types resolve to server with an info
  note (both re-export them) instead of an action-required warning; 'unknown' still warns. The scan is
  bounded (skips heavy dirs, file budget, early-exit). On firebase this cuts the codemod diagnostics from
  14 to 2 with 0 introduced typecheck errors.
The handler-registration transform rewrites setRequestHandler(XSchema, …) and
  setNotificationHandler(XSchema, …) to the v2 spec form via a schema→method table. The task schemas were
  missing, so a handler like setNotificationHandler(TaskStatusNotificationSchema, …) fell through to the
  generic "use the 3-argument form" diagnostic and was left for manual migration.

Add the task entries: tasks/get, tasks/result, tasks/list, tasks/cancel, and
  notifications/tasks/status. These are spec methods (the request schemas are members of
  ServerRequestSchema and the notification is in the notification union), so the rewritten two-argument
  call resolves to the spec overload of setRequestHandler/setNotificationHandler and typechecks.

Co-Authored-By: Felix Weinberger <fweinberger@anthropic.com>
@KKonstantinov KKonstantinov requested a review from a team as a code owner June 24, 2026 07:58
@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 68b2120

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@modelcontextprotocol/core Minor
@modelcontextprotocol/core-internal Major
@modelcontextprotocol/client Major
@modelcontextprotocol/codemod Minor
@modelcontextprotocol/server Major
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2354

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2354

@modelcontextprotocol/core

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/core@2354

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2354

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2354

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2354

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2354

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2354

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2354

commit: 68b2120

@KKonstantinov

Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/sdk-shared/src/index.ts Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts Outdated
Comment thread docs/superpowers/plans/2026-06-02-readbuffer-max-size.md Outdated
… arg, preserve import

  comments

- importPaths: aliased named imports now route through the per-symbol splitter (addOrMergeImport
  carries aliases), so a mixed type+schema import no longer collapses into one v2 package and mis-routes
  schema constants
  - schemaParamRemoval: drop a literal `undefined` result-schema argument from request()/callTool() when
  an options arg follows
  - importPaths: preserve the first SDK import's leading header/JSDoc comment across the rewrite
  - batch-test: install pnpm clones standalone (--ignore-workspace --no-frozen-lockfile, CI=true) so repos
  nested in this workspace actually install
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts
Comment thread packages/core/src/exports/public/index.ts
Comment thread packages/sdk-shared/README.md Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts Outdated
Comment thread packages/codemod/src/utils/detectFormatter.ts
Comment thread packages/core/README.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stoked about this

mattzcarey
mattzcarey previously approved these changes Jun 25, 2026
@KKonstantinov

Copy link
Copy Markdown
Contributor Author

Handling some of the edge cases raised by claude[bot], then renaming core to core-internal and sdk-shared to core

@KKonstantinov KKonstantinov dismissed stale reviews from felixweinberger and mattzcarey via 065e142 June 25, 2026 10:20
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts Outdated
Comment thread typedoc.config.mjs Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts Outdated
Comment thread packages/codemod/test/detectFormatter.test.ts Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts
Comment thread packages/core/package.json
Comment thread docs/migration-SKILL.md Outdated
… into feature/codemod-iterations-3

# Conflicts:
#	packages/client/test/client/streamableHttp.test.ts
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts Outdated
Comment thread packages/core/src/index.ts Outdated
@KKonstantinov

Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread scripts/fetch-spec-types.ts
@KKonstantinov

Copy link
Copy Markdown
Contributor Author

@claude review

1 similar comment
@KKonstantinov

Copy link
Copy Markdown
Contributor Author

@claude review

…iddleware

  eslint

  Follow-ups to the core→core-internal / sdk-shared→core rename:
  - CLAUDE.md exports section was missing the public @modelcontextprotocol/core package
  - middleware express/fastify/hono internal-regex still listed a vestigial 'core' the sweep
    could not reach (non-contiguous substring inside the alternation)
@KKonstantinov

Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/core-internal/src/index.ts Outdated
Comment thread packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts
…red rename

  changeset

  - core index.ts/tsdown.config.ts/test comments pointed at core/src/... paths that
    now live under core-internal/src/...; renamed sdkSharedSchemas→coreSchemas refs
  - codemod drift-guard tests carried stale sdkShared* variable names
  - @modelcontextprotocol/sdk-shared was never published to npm, so the rename
    changeset's 'migrate your imports' entry would mislead the first core changelog;
    add-core-public-package.md already provides a single accurate 'new package' entry
…ale validator

  comment

  - mockPaths: import('…/types.js').then(({ CallToolResultSchema }) => …) was silently
    rewritten to the context package (server/client, no Zod schema exports) with no diagnostic.
    Generalize destructure handling (getModuleBindingPattern) so a .then(({…}) => …) param is
    routed/renamed per-symbol like 'const { … } = await import()' — schema-only → core, mixed
    flagged. +3 tests.
  - core-internal index.ts: validator-import comment listed @modelcontextprotocol/core/validators/*,
    which doesn't exist after the rename (core exports only '.'); point at {client,server}.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found on the latest revision — the fixes for the earlier findings (auth-schema routing, JSONRPCResponseSchema rename, lazy context resolution, the fetch-spec-types output path, and the rename leftovers) all look incorporated. That said, this PR introduces a new published package and renames the internal core package, so a maintainer should sign off on the public API surface and the rename before merge.

Extended reasoning...

Overview

This PR (276 files) introduces @modelcontextprotocol/core as the new public home for the spec + OAuth/OpenID Zod schemas, renames the private internal barrel to @modelcontextprotocol/core-internal, and makes a series of codemod accuracy/correctness changes (per-symbol schema routing, project-type inference from source, task-handler method mapping, formatter detection, package.json reconciliation against surviving imports). The latest commits address the findings raised in earlier review rounds — including the previously-missing packages/core-internal directory, the OAuth schema routing gap, the JSONRPCResponseSchema semantic-widening rename, the unguarded undefined-arg removal, the eager project-type diagnostics, the fetch-spec-types.ts output path, and the destructured .then() dynamic-import routing.

Security risks

Low. No auth/crypto logic changes — the OAuth Zod schemas are only re-exported from a new package, not modified. The codemod changes only affect migration tooling that rewrites user source locally; there is no network or credential handling introduced.

Level of scrutiny

High, and human-level. The PR creates a brand-new published npm package with a deliberately curated export surface, takes over the clean core package name, and bumps nine packages via changesets. Per the repo's own conventions (minimalism, burden of proof on addition, every export intentional), the decision to publish the raw Zod schemas as a standalone package — and the bundling-vs-external-dependency trade-off described in the PR — is a design call that maintainers should explicitly endorse, not something to shadow-approve.

Other factors

The bug-hunting system found no new issues on this revision, codemod/core test coverage was substantially expanded (drift guards, batch test against firebase-tools), and the author has been responsive to every prior review round. The remaining open questions are design/ownership questions rather than correctness defects.

@felixweinberger felixweinberger merged commit 0fb8406 into main Jun 25, 2026
19 checks passed
@felixweinberger felixweinberger deleted the feature/codemod-iterations-3 branch June 25, 2026 18:42
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.

3 participants