v2: codemod iterations, canonical zod schema exports from sdk-shared#2354
v2: codemod iterations, canonical zod schema exports from sdk-shared#2354KKonstantinov wants to merge 8 commits into
sdk-shared#2354Conversation
… into feature/codemod-iterations-3
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>
🦋 Changeset detectedLatest commit: 5ffdb38 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
@claude review |
| 'MultiSelectEnumSchemaSchema', | ||
| 'NotificationSchema', | ||
| 'NumberSchemaSchema', | ||
| 'OAuthClientInformationFullSchema', | ||
| 'OAuthClientInformationSchema', | ||
| 'OAuthClientMetadataSchema', | ||
| 'OAuthClientRegistrationErrorSchema', | ||
| 'OAuthErrorResponseSchema', | ||
| 'OAuthMetadataSchema', | ||
| 'OAuthProtectedResourceMetadataSchema', | ||
| 'OAuthTokenRevocationRequestSchema', | ||
| 'OAuthTokensSchema', | ||
| 'OpenIdProviderDiscoveryMetadataSchema', | ||
| 'OpenIdProviderMetadataSchema', | ||
| 'PaginatedRequestParamsSchema', | ||
| 'PaginatedRequestSchema', | ||
| 'PaginatedResultSchema', |
There was a problem hiding this comment.
🔴 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
- Routing. The
importMap.tsentry for'@modelcontextprotocol/sdk/shared/auth.js'is plainRESOLVE_BY_CONTEXTwith noschemaSymbolTarget(the newschemaSymbolTargetrouting only exists on thetypes.jsmapping). Soimport { OAuthTokensSchema } from '@modelcontextprotocol/sdk/shared/auth.js'is rewritten to@modelcontextprotocol/clientor@modelcontextprotocol/server. - Those packages don't export the schema values.
packages/core/src/exports/public/index.ts:19explicitly exports only the "Auth TypeScript types (NOT Zod schemas like OAuthMetadataSchema)", and the client/server barrels just re-exportcore/public— there is noOAuthTokensSchemavalue export anywhere on that path. sdk-shareddoesn't have them either. The OAuth Zod schemas are defined inpackages/core/src/shared/auth.ts(e.g.OAuthTokensSchemaat line 131), not incore/src/types/schemas.ts, which is the only modulesdk-shared's alias bundles.packages/sdk-shared/src/index.tsexports noOAuth*Schema/OpenId*Schemanames, and the drift-guard test only readsschemas.ts, so it cannot catch the gap. Even if theshared/auth.jsmapping had aschemaSymbolTarget, this PR removes all 11 OAuth*/OpenId* schema names fromSPEC_SCHEMA_NAMES(this hunk), so the membership check would not route them anyway.- No diagnostic. The clean per-symbol resolution path is silent, and the deleted
specSchemaAccesstransform 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);importPathsTransformlooks up'@modelcontextprotocol/sdk/shared/auth.js'→{ target: 'RESOLVE_BY_CONTEXT', status: 'moved' }(importMap.ts:149-152). NoschemaSymbolTarget, sosymbolTargetOverride('OAuthTokensSchema', mapping)returnsundefined.- The symbol resolves by context, e.g. to
@modelcontextprotocol/server. Output:import { OAuthTokensSchema } from '@modelcontextprotocol/server';— emitted with no diagnostic. - No other transform touches the usage:
specSchemaAccessis deleted, andSPEC_SCHEMA_NAMESno longer containsOAuthTokensSchema. tscon 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.
There was a problem hiding this comment.
don't think we should commit superpowers stuff
Introduce
@modelcontextprotocol/sdk-sharedas 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/serverand@modelcontextprotocol/clientdeliberately 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 likeCallToolResultSchema.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:
@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*SchemaZod 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 onserver/client. The package is runtime-neutral withzodas its only runtime dependency.sdk-shared.*Schemasymbols imported from@modelcontextprotocol/sdk/types.jsare now routed to@modelcontextprotocol/sdk-shared(a mixedimport { CallToolResult, CallToolResultSchema }is split — the type resolves by context, the schema to sdk-shared). The oldspecSchemaAccesstransform and its generated schema-map are removed in favor of this behavior-preserving import swap.firebase/firebase-tools):@modelcontextprotocol/sdk/typesas well as.../types.js).@modelcontextprotocol/sdkdependency, so detection frompackage.jsoncame 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.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.import * as t … t.CallToolResultSchema.parse()), which can't be split automatically, with an actionable diagnostic pointing at sdk-shared.sdk-shared;.parse()unchanged), with the Zod-freeisSpecType/specTypeSchemasas an alternative.How Has This Been Tested?
sdk-sharedtests (schema round-trip + a drift guard asserting every spec schema is re-exported).build:all,check:all(typecheck + lint + docs) all green. Thesdk-sharedbundle was verified runtime-neutral —zodexternal, no Node built-ins, noProtocol/transport/validator code pulled in, and the.d.tsexposes the schemas as real values (not type-only aliases).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,ErrorCodesplit). Spot-checked the migrated output: schemas import from@modelcontextprotocol/sdk-sharedand.parse()calls are preserved verbatim.Breaking Changes
None for existing v2 consumers.
@modelcontextprotocol/sdk-sharedis 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
Checklist
Additional context
sdk-sharedbundles core rather than owning the schemas. An alternative was prototyped wheresdk-sharedowned the schemas andserver/clientreferenced it externally to avoid duplicating the schema bytes. It worked but was invasive (it touchedcore/server/client/middleware build configs and required guarding against a rolldown value-export degradation). This PR takes the lower-footprint approach:sdk-sharedbundles the (private) core schema module via a narrowed alias and re-exports only the*Schemavalues, leavingserver/clientuntouched. The trade-off is thatserver/clientkeep their own internally-bundled schema copies (not part of their public API) — an accepted cost for a much smaller, lower-risk change.