Skip to content
5 changes: 5 additions & 0 deletions .changeset/codemod-import-specifier-normalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/codemod": patch
---

The v1→v2 codemod now normalizes import specifiers before the import-map lookup, so extensionless (`@modelcontextprotocol/sdk/types`) and directory-style (`@modelcontextprotocol/sdk/server`) specifiers resolve the same as their canonical `.js` form. Projects using bundler/node16 module resolution that imported SDK modules without the `.js` extension previously hit "Unknown SDK import path: ... Manual migration required" even though the `.js` twin was mapped; those now migrate automatically. Genuinely unknown subpaths still report.
5 changes: 5 additions & 0 deletions .changeset/codemod-project-type-inference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/codemod": patch
---

The v1→v2 codemod now infers whether a project is client, server, or both by scanning the source for `@modelcontextprotocol/sdk/client/` and `.../server/` imports when the split v2 dependencies are not yet present in `package.json`. A v1 project (single `@modelcontextprotocol/sdk` dependency) previously resolved to `unknown`, so every file importing only shared protocol types defaulted to `@modelcontextprotocol/server` with an action-required warning. Now a project that uses both client and server APIs is detected as `both` and resolves shared types to the server package with an informational note (both packages re-export them); a client-only or server-only project routes shared types to the package it actually installs.
5 changes: 5 additions & 0 deletions .changeset/codemod-spec-schema-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/codemod": patch
---

The v1→v2 codemod now migrates spec-schema `.parse()` / `.safeParse()` usage by renaming the schema reference to `specTypeSchemas.X` and leaving the call and its result access untouched, instead of rewriting to `['~standard'].validate()` and remapping `.success`/`.data`/`.error`. This pairs with `specTypeSchemas` entries now exposing those Zod-compatible methods, so the migration is a behavior-preserving rename: `.parse()` still throws on invalid input and `.safeParse()` keeps its discriminated result, with no `.parse()` sites left unmigrated. Other Zod methods that are not exposed on the entry (e.g. `.extend`, `.parseAsync`) are renamed and flagged inline for manual rewrite.
5 changes: 5 additions & 0 deletions .changeset/codemod-task-method-strings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/codemod": patch
---

The v1→v2 codemod's handler-registration transform now recognizes the task spec methods (`tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`, and the `notifications/tasks/status` notification). `setRequestHandler`/`setNotificationHandler` calls passing a task schema are rewritten to the v2 method-string form instead of falling through to a manual-migration diagnostic.
7 changes: 7 additions & 0 deletions .changeset/specschemas-zod-compat-parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/client": minor
"@modelcontextprotocol/server": minor
---

Expose Zod-compatible `parse()` / `safeParse()` on every `specTypeSchemas` entry. The schemas are still typed as Standard Schema (`['~standard'].validate()` remains the recommended, library-agnostic API), but the underlying runtime values are Zod schemas, so these two methods are now surfaced with their original behavior — `parse()` returns the typed value or throws a `ZodError`, `safeParse()` returns the `{ success, data } | { success, error }` result. This lets code written against the previous top-level `*Schema` exports migrate by a reference rename (`CallToolResultSchema.parse(x)` → `specTypeSchemas.CallToolResult.parse(x)`) with identical behavior, instead of being rewritten to `['~standard'].validate()` with manual remapping of `.success`/`.data`/`.error`. Only these two methods are exposed; the rest of the Zod schema surface stays internal.
20 changes: 18 additions & 2 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,24 @@ import { specTypeSchemas } from '@modelcontextprotocol/client';
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
```

`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously,
so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
If your v1 code called `.parse()` or `.safeParse()` on a `*Schema` constant, the smallest migration is
to rename the reference — `specTypeSchemas.X` retains Zod-compatible `.parse()` and `.safeParse()` with
identical behavior (`.parse()` still throws a `ZodError` on invalid input):

```typescript
// v1
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
const tool = CallToolResultSchema.parse(value);
const r = CallToolResultSchema.safeParse(value); // { success, data } | { success, error }

// v2 — rename only; .parse()/.safeParse() and their result shapes are unchanged
import { specTypeSchemas } from '@modelcontextprotocol/client';
const tool = specTypeSchemas.CallToolResult.parse(value);
const r = specTypeSchemas.CallToolResult.safeParse(value);
```

`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (also exposing the `.parse()`/`.safeParse()` shown above) — `validate()` returns the result synchronously,
so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. New code should prefer the library-agnostic `['~standard'].validate()` or `isSpecType`. The pre-existing `isCallToolResult(value)` guard still works.

### Client list methods return empty results for missing capabilities

Expand Down
19 changes: 19 additions & 0 deletions packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,22 @@ for (const barrelSpecifier of ['@modelcontextprotocol/sdk/validation/index.js',
export function isAuthImport(specifier: string): boolean {
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
}

/**
* Look up a v1 import specifier in {@link IMPORT_MAP}, tolerating module-resolution variants.
*
* `IMPORT_MAP` is keyed on the canonical `.js`-suffixed file form (e.g. `.../types.js`,
* `.../server/index.js`). But v1 consumers using bundler/`node16` resolution frequently import the
* same module without the extension (`.../types`) or in directory form (`.../server`, which resolves
* to `server/index.js`). An exact-string lookup misses those and reports "Unknown SDK import path"
* even though the `.js` twin is mapped. Normalize before giving up: try the literal key, then the
* `.js`-file form, then the `/index.js` directory form.
*/
export function resolveImportMapping(specifier: string): ImportMapping | undefined {
const direct = IMPORT_MAP[specifier];
if (direct) return direct;
if (!specifier.startsWith('@modelcontextprotocol/sdk') || /\.(js|mjs|cjs)$/.test(specifier)) {
return undefined;
}
return IMPORT_MAP[`${specifier}.js`] ?? IMPORT_MAP[`${specifier}/index.js`];
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export const SCHEMA_TO_METHOD: Record<string, string> = {
SetLevelRequestSchema: 'logging/setLevel',
PingRequestSchema: 'ping',
CompleteRequestSchema: 'completion/complete',
ListRootsRequestSchema: 'roots/list'
ListRootsRequestSchema: 'roots/list',
GetTaskRequestSchema: 'tasks/get',
GetTaskPayloadRequestSchema: 'tasks/result',
ListTasksRequestSchema: 'tasks/list',
CancelTaskRequestSchema: 'tasks/cancel'
};

export const NOTIFICATION_SCHEMA_TO_METHOD: Record<string, string> = {
Expand All @@ -27,5 +31,6 @@ export const NOTIFICATION_SCHEMA_TO_METHOD: Record<string, string> = {
CancelledNotificationSchema: 'notifications/cancelled',
InitializedNotificationSchema: 'notifications/initialized',
RootsListChangedNotificationSchema: 'notifications/roots/list_changed',
ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete'
ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete',
TaskStatusNotificationSchema: 'notifications/tasks/status'
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { renameAllReferences } from '../../../utils/astUtils.js';
import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics.js';
import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js';
import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js';
import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js';
import { isAuthImport, resolveImportMapping } from '../mappings/importMap.js';
import { SIMPLE_RENAMES } from '../mappings/symbolMap.js';

const REEXPORT_WARNINGS: Record<string, string> = {
Expand Down Expand Up @@ -71,7 +71,7 @@ export const importPathsTransform: Transform = {
const defaultImport = imp.getDefaultImport();
const namespaceImport = imp.getNamespaceImport();

let mapping = IMPORT_MAP[specifier];
let mapping = resolveImportMapping(specifier);

if (!mapping && isAuthImport(specifier)) {
mapping = {
Expand Down Expand Up @@ -223,7 +223,7 @@ function rewriteExportDeclarations(
if (!specifier) continue;

const line = exp.getStartLineNumber();
let mapping = IMPORT_MAP[specifier];
let mapping = resolveImportMapping(specifier);

if (!mapping && isAuthImport(specifier)) {
mapping = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Diagnostic, Transform, TransformContext, TransformResult } from '.
import { actionRequired, v2Gap, warning } from '../../../utils/diagnostics.js';
import { isSdkSpecifier } from '../../../utils/importUtils.js';
import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js';
import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js';
import { isAuthImport, resolveImportMapping } from '../mappings/importMap.js';
import { SIMPLE_RENAMES } from '../mappings/symbolMap.js';

const MOCK_METHODS = new Set([
Expand Down Expand Up @@ -58,7 +58,7 @@ function resolveTarget(
| { target: string; renamedSymbols?: Record<string, string>; symbolTargetOverrides?: Record<string, string> }
| { removed: true; isV2Gap?: boolean; removalMessage?: string }
| null {
const mapping = IMPORT_MAP[specifier];
const mapping = resolveImportMapping(specifier);
if (!mapping && isAuthImport(specifier)) return { target: '@modelcontextprotocol/server-legacy/auth' };
if (!mapping) return null;
if (mapping.status === 'removed') return { removed: true, isV2Gap: mapping.isV2Gap, removalMessage: mapping.removalMessage };
Expand Down
Loading
Loading