Skip to content

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

Open
KKonstantinov wants to merge 8 commits into
mainfrom
feature/codemod-iterations-3
Open

v2: codemod iterations, canonical zod schema exports from sdk-shared#2354
KKonstantinov wants to merge 8 commits into
mainfrom
feature/codemod-iterations-3

Conversation

@KKonstantinov

Copy link
Copy Markdown
Contributor

Introduce @modelcontextprotocol/sdk-shared as the public home for the MCP specification Zod schemas, and teach the v1→v2 codemod to migrate schema usage to it — plus several codemod accuracy fixes surfaced by running the migration against real-world v1 repos.

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.

This PR closes that gap and tightens the migration:

  • New package @modelcontextprotocol/sdk-shared — the canonical, public home for the spec Zod schemas. 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. 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.
  • Codemod: route schema imports to sdk-shared. *Schema symbols imported from @modelcontextprotocol/sdk/types.js are now routed to @modelcontextprotocol/sdk-shared (a mixed import { CallToolResult, CallToolResultSchema } is split — the type resolves by context, the schema to sdk-shared). 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 sdk-shared.
  • Migration docs updated to describe the schema-validation path (import from sdk-shared; .parse() unchanged), with the Zod-free isSpecType / specTypeSchemas as an alternative.

How Has This Been Tested?

  • Unit tests: the codemod suite passes (import routing, schema→method mapping, project-type inference incl. comment/extensionless edge cases, namespace-import diagnostics), plus sdk-shared tests (schema round-trip + a drift guard asserting every spec schema is re-exported).
  • Full monorepo gate: build:all, check:all (typecheck + lint + docs) all green. The sdk-shared 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/sdk-shared and .parse() calls are preserved verbatim.

Breaking Changes

None for existing v2 consumers. @modelcontextprotocol/sdk-shared is a new, additive package; core, server, client, and the middleware packages are unchanged. The schema-import-location change it 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 sdk-shared bundles core rather than owning the schemas. An alternative was prototyped where sdk-shared owned the schemas and server/client referenced it externally to avoid duplicating the schema bytes. It worked but was invasive (it touched core/server/client/middleware build configs and required guarding against a rolldown value-export degradation). This PR takes the lower-footprint approach: sdk-shared bundles the (private) core schema module via a narrowed alias and re-exports only the *Schema values, leaving server/client untouched. 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: 5ffdb38

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

This PR includes changesets to release 2 packages
Name Type
@modelcontextprotocol/sdk-shared Minor
@modelcontextprotocol/codemod Minor

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/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: 5ffdb38

@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
Comment on lines 91 to 96
'MultiSelectEnumSchemaSchema',
'NotificationSchema',
'NumberSchemaSchema',
'OAuthClientInformationFullSchema',
'OAuthClientInformationSchema',
'OAuthClientMetadataSchema',
'OAuthClientRegistrationErrorSchema',
'OAuthErrorResponseSchema',
'OAuthMetadataSchema',
'OAuthProtectedResourceMetadataSchema',
'OAuthTokenRevocationRequestSchema',
'OAuthTokensSchema',
'OpenIdProviderDiscoveryMetadataSchema',
'OpenIdProviderMetadataSchema',
'PaginatedRequestParamsSchema',
'PaginatedRequestSchema',
'PaginatedResultSchema',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 v1 code that imports the OAuth Zod schemas (e.g. OAuthTokensSchema, OAuthMetadataSchema) from @modelcontextprotocol/sdk/shared/auth.js and calls .parse()/.parseAsync() now migrates into a silent "has no exported member" typecheck error: this PR removes the OAuth*/OpenId* names from SPEC_SCHEMA_NAMES, deletes the specSchemaAccess transform that previously rewrote those usages with a diagnostic, and sdk-shared does not export the auth schemas (they live in core/src/shared/auth.ts, not the bundled schemas.ts), so the import is rerouted by context to @modelcontextprotocol/client/server, which export only the OAuth TypeScript types. Either export the auth Zod schemas from sdk-shared (re-adding the names to SPEC_SCHEMA_NAMES and giving the shared/auth.js mapping a schemaSymbolTarget), or at minimum emit an actionRequired diagnostic for *Schema symbols imported from sdk/shared/auth.js.

Extended reasoning...

What the bug is

In v1, the OAuth Zod schema constants (OAuthTokensSchema, OAuthMetadataSchema, OAuthClientInformationSchema, OAuthClientMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthErrorResponseSchema, OpenIdProviderMetadataSchema, etc.) were exported from @modelcontextprotocol/sdk/shared/auth.js, and real-world code calls e.g. OAuthTokensSchema.parseAsync(data) — the specSchemaAccess.test.ts file deleted by this PR covered exactly that case. After this PR, the codemod has no migration path for those usages: the migrated import points at a package that does not export the schema value, with no diagnostic telling the user anything went wrong.

The code path

  1. Routing. The importMap.ts entry for '@modelcontextprotocol/sdk/shared/auth.js' is plain RESOLVE_BY_CONTEXT with no schemaSymbolTarget (the new schemaSymbolTarget routing only exists on the types.js mapping). So import { OAuthTokensSchema } from '@modelcontextprotocol/sdk/shared/auth.js' is rewritten to @modelcontextprotocol/client or @modelcontextprotocol/server.
  2. Those packages don't export the schema values. packages/core/src/exports/public/index.ts:19 explicitly exports only the "Auth TypeScript types (NOT Zod schemas like OAuthMetadataSchema)", and the client/server barrels just re-export core/public — there is no OAuthTokensSchema value export anywhere on that path.
  3. sdk-shared doesn't have them either. The OAuth Zod schemas are defined in packages/core/src/shared/auth.ts (e.g. OAuthTokensSchema at line 131), not in core/src/types/schemas.ts, which is the only module sdk-shared's alias bundles. packages/sdk-shared/src/index.ts exports no OAuth*Schema/OpenId*Schema names, and the drift-guard test only reads schemas.ts, so it cannot catch the gap. Even if the shared/auth.js mapping had a schemaSymbolTarget, this PR removes all 11 OAuth*/OpenId* schema names from SPEC_SCHEMA_NAMES (this hunk), so the membership check would not route them anyway.
  4. No diagnostic. The clean per-symbol resolution path is silent, and the deleted specSchemaAccess transform was the component that previously emitted a diagnostic and rewrote the usage.

Why this is a regression introduced by this PR

Pre-PR, the generated SPEC_SCHEMA_NAMES included the OAuth/OpenId schema names (the deleted scripts/generateSpecSchemaMap.ts explicitly merged authSchemas from core's specTypeSchema.ts, which still contains them at lines 190-205), and the specSchemaAccess transform rewrote OAuthTokensSchema.parseAsync(data) to specTypeSchemas.OAuthTokens.parseAsync(data), removed the now-unused import, and emitted a warning/actionRequired diagnostic — so the migrated code always had a resolvable import plus a pointer to what needed manual attention. Post-PR, the same input compiles to a dangling import with zero diagnostics. It also contradicts the migration docs updated in this PR, which promise that the *Schema constants migrate as a behavior-preserving import-path swap with .parse()/.safeParse() unchanged.

Step-by-step proof

Given this v1 file:

import { OAuthTokensSchema } from '@modelcontextprotocol/sdk/shared/auth.js';
const tokens = await OAuthTokensSchema.parseAsync(data);
  1. importPathsTransform looks up '@modelcontextprotocol/sdk/shared/auth.js'{ target: 'RESOLVE_BY_CONTEXT', status: 'moved' } (importMap.ts:149-152). No schemaSymbolTarget, so symbolTargetOverride('OAuthTokensSchema', mapping) returns undefined.
  2. The symbol resolves by context, e.g. to @modelcontextprotocol/server. Output: import { OAuthTokensSchema } from '@modelcontextprotocol/server'; — emitted with no diagnostic.
  3. No other transform touches the usage: specSchemaAccess is deleted, and SPEC_SCHEMA_NAMES no longer contains OAuthTokensSchema.
  4. tsc on the migrated project: Module '"@modelcontextprotocol/server"' has no exported member 'OAuthTokensSchema' — a codemod-introduced compile break the user has to debug from scratch. The PR's own batch test (firebase-tools) wouldn't catch this unless that repo happens to use the OAuth schemas.

Impact

Any v1 project doing runtime validation of OAuth metadata/token responses with the SDK's auth schemas — a documented and common pattern for clients implementing the OAuth flow — gets a silent compile break from the codemod, and the migration docs give them the wrong expectation that the schema usage "just works" after the import swap.

How to fix

Either (a) export the auth Zod schemas from @modelcontextprotocol/sdk-shared (widen the alias/bundle to include core/src/shared/auth.ts), re-add the 11 OAuth*/OpenId* names to SPEC_SCHEMA_NAMES, and give the '@modelcontextprotocol/sdk/shared/auth.js' mapping a schemaSymbolTarget: '@modelcontextprotocol/sdk-shared'; or (b) at minimum emit an actionRequired diagnostic when a *Schema symbol is imported from sdk/shared/auth.js, so users aren't silently broken.

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.

don't think we should commit superpowers stuff

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.

2 participants