Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ jobs:
- name: Publish preview packages
run:
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
'./packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'
'./packages/codemod' './packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'
5 changes: 5 additions & 0 deletions packages/codemod/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @ts-check

import baseConfig from '@modelcontextprotocol/eslint-config';

export default [...baseConfig];
66 changes: 66 additions & 0 deletions packages/codemod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@modelcontextprotocol/codemod",
"version": "2.0.0-alpha.0",
"description": "Codemod to migrate MCP TypeScript SDK code from v1 to v2",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
},
"engines": {
"node": ">=20"
},
"keywords": [
"modelcontextprotocol",
"mcp",
"codemod",
"migration"
],
"bin": {
"mcp-codemod": "./dist/cli.mjs"
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": [
"dist"
],
Comment on lines +32 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new @modelcontextprotocol/codemod package is added to the publish workflow but ships with no README.md, while every other published package in the repo (client, server, middleware/*) has one. The PR description's CLI usage, programmatic API, and transform table won't be visible on the npm package page — worth adding a packages/codemod/README.md before this lands (npm auto-includes README regardless of the files array, so no package.json change is needed).

Extended reasoning...

What the gap is

This PR adds @modelcontextprotocol/codemod as a new published package — it has a bin entry (mcp-codemod), a programmatic API export, and is added to .github/workflows/publish.yml:43 so pkg-pr-new is already publishing preview builds. But packages/codemod/ contains no README.md. Every other published package in the repo has one: packages/client/README.md, packages/server/README.md, and all four packages/middleware/*/README.md files exist.

The repo's own review checklist (REVIEW.md) explicitly calls this out: "New feature: verify prose documentation is added (not just JSDoc), and assess whether examples/ needs a new or updated example." A new package with both a CLI and a programmatic API is squarely in scope for that item.

Why it matters

The PR description already contains exactly the documentation users need — the CLI Usage block (mcp-codemod v1-to-v2 ./src, --dry-run, --transforms, --list, --ignore), the Programmatic API snippet (getMigration / run), and the 9-row transform table. None of that will be visible on npm. A user who runs npx @modelcontextprotocol/codemod and gets the commander help text will have nowhere to look for the transform IDs, the design decisions, or the migration scope.

Step-by-step proof

  1. .github/workflows/publish.yml:43 adds './packages/codemod' to the pkg-pr-new publish command, so the package is published on every PR push (and presumably on release).
  2. packages/codemod/package.json:32-34 sets "files": ["dist"] and a bin entry — so the package is intended for end-user consumption via npx.
  3. ls packages/codemod/ shows: eslint.config.mjs, package.json, src/, test/, tsconfig.json, tsdown.config.ts, typedoc.json, vitest.config.js — no README.md.
  4. ls packages/*/README.md packages/middleware/*/README.md shows READMEs for client, server, express, fastify, hono, and node — codemod is the only published package without one.
  5. On npm, the package page renders the README as the primary documentation surface. With no README, the page shows only the package.json description string: "Codemod to migrate MCP TypeScript SDK code from v1 to v2" — no usage, no flags, no transform list.

Note on files array

npm always includes README* (along with package.json, LICENSE*, and the main entry) regardless of the files allowlist, so adding packages/codemod/README.md is sufficient — no need to touch "files": ["dist"].

Fix

Add packages/codemod/README.md containing (at minimum) the CLI Usage block, the Programmatic API snippet, and the transform table from this PR's description. Given the PR is explicitly titled "draft" and the author has an open TODO checklist, this is likely already planned — flagging it here per the REVIEW.md checklist item so it doesn't slip.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TBD. Add README.md once the feasibility of the codemod has been confirmed.

"scripts": {
"typecheck": "tsgo -p tsconfig.json --noEmit",
"build": "tsdown",
"build:watch": "tsdown --watch",
"prepack": "pnpm run build",
"lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .",
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .",
"check": "pnpm run typecheck && pnpm run lint",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"commander": "^13.0.0",
"ts-morph": "^28.0.0"
},
"devDependencies": {
"@modelcontextprotocol/tsconfig": "workspace:^",
"@modelcontextprotocol/vitest-config": "workspace:^",
"@modelcontextprotocol/eslint-config": "workspace:^",
"@eslint/js": "catalog:devTools",
"@typescript/native-preview": "catalog:devTools",
"eslint": "catalog:devTools",
"eslint-config-prettier": "catalog:devTools",
"eslint-plugin-n": "catalog:devTools",
"prettier": "catalog:devTools",
"tsdown": "catalog:devTools",
"tsx": "catalog:devTools",
"typescript": "catalog:devTools",
"typescript-eslint": "catalog:devTools",
"vitest": "catalog:devTools"
}
}
130 changes: 130 additions & 0 deletions packages/codemod/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env node

import { existsSync, statSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';

import { Command } from 'commander';

import { listMigrations } from './migrations/index.js';
import { run } from './runner.js';
import { DiagnosticLevel } from './types.js';
import { formatDiagnostic } from './utils/diagnostics.js';

const require = createRequire(import.meta.url);
const { version } = require('../package.json') as { version: string };

const program = new Command();

program.name('mcp-codemod').description('Codemod to migrate MCP TypeScript SDK code between versions').version(version);

for (const [name, migration] of listMigrations()) {
program
.command(`${name} [target-dir]`)
.description(migration.description)
.option('-d, --dry-run', 'Preview changes without writing files')
.option('-t, --transforms <ids>', 'Comma-separated transform IDs to run (default: all)')
.option('-v, --verbose', 'Show detailed per-change output')
.option('--ignore <patterns...>', 'Additional glob patterns to ignore')
.option('--list', 'List available transforms for this migration')
.action((targetDir: string | undefined, opts: Record<string, unknown>) => {
try {
if (opts['list']) {
console.log(`\nAvailable transforms for ${name}:\n`);
for (const t of migration.transforms) {
console.log(` ${t.id.padEnd(20)} ${t.name}`);
}
console.log('');
return;
}

if (!targetDir) {
console.error(`\nError: missing required argument <target-dir>.\n`);
process.exitCode = 1;
return;
}

const resolvedDir = path.resolve(targetDir);

if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
console.error(`\nError: "${resolvedDir}" is not a valid directory.\n`);
process.exitCode = 1;
return;
}

console.log(`\n@modelcontextprotocol/codemod — ${migration.name}\n`);
console.log(`Scanning ${resolvedDir}...`);
if (opts['dryRun']) {
console.log('(dry run — no files will be modified)\n');
} else {
console.log('');
}

const transforms = opts['transforms'] ? (opts['transforms'] as string).split(',').map(s => s.trim()) : undefined;

const result = run(migration, {
targetDir: resolvedDir,
dryRun: opts['dryRun'] as boolean | undefined,
verbose: opts['verbose'] as boolean | undefined,
transforms,
ignore: opts['ignore'] as string[] | undefined
});

if (result.filesChanged === 0 && result.diagnostics.length === 0) {
console.log('No changes needed — code already migrated or no SDK imports found.\n');
return;
}

if (result.filesChanged > 0) {
console.log(`Changes: ${result.totalChanges} across ${result.filesChanged} file(s)\n`);
}

if (opts['verbose']) {
console.log('Files modified:');
for (const fr of result.fileResults) {
console.log(` ${fr.filePath} (${fr.changes} change(s))`);
}
console.log('');
}

const errors = result.diagnostics.filter(d => d.level === DiagnosticLevel.Error);
if (errors.length > 0) {
console.log(`Errors (${errors.length}):`);
for (const d of errors) {
console.log(formatDiagnostic(d));
}
console.log('');
process.exitCode = 1;
}

const warnings = result.diagnostics.filter(d => d.level === DiagnosticLevel.Warning);
if (warnings.length > 0) {
console.log(`Warnings (${warnings.length}):`);
for (const d of warnings) {
console.log(formatDiagnostic(d));
}
console.log('');
}

const infos = result.diagnostics.filter(d => d.level === DiagnosticLevel.Info);
if (infos.length > 0) {
console.log(`Info (${infos.length}):`);
for (const d of infos) {
console.log(formatDiagnostic(d));
}
console.log('');
}
Comment thread
claude[bot] marked this conversation as resolved.

if (opts['dryRun']) {
console.log('Run without --dry-run to apply changes.\n');
} else {
console.log('Migration complete. Review the changes and run your build/tests.\n');
}
} catch (error) {
console.error(`\nError: ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
});
}

program.parse();
13 changes: 13 additions & 0 deletions packages/codemod/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { getMigration, listMigrations } from './migrations/index.js';
export { run } from './runner.js';
export type {
Diagnostic,
FileResult,
Migration,
RunnerOptions,
RunnerResult,
Transform,
TransformContext,
TransformResult
} from './types.js';
export { DiagnosticLevel } from './types.js';
12 changes: 12 additions & 0 deletions packages/codemod/src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Migration } from '../types.js';
import { v1ToV2Migration } from './v1-to-v2/index.js';

const migrations = new Map<string, Migration>([['v1-to-v2', v1ToV2Migration]]);

export function getMigration(name: string): Migration | undefined {
return migrations.get(name);
}

export function listMigrations(): Map<string, Migration> {
return migrations;
}
8 changes: 8 additions & 0 deletions packages/codemod/src/migrations/v1-to-v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Migration } from '../../types.js';
import { v1ToV2Transforms } from './transforms/index.js';

export const v1ToV2Migration: Migration = {
name: 'v1-to-v2',
description: 'Migrate from @modelcontextprotocol/sdk (v1) to v2 packages (@modelcontextprotocol/client, /server, etc.)',
transforms: v1ToV2Transforms
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface ContextMapping {
from: string;
to: string;
}

export const CONTEXT_PROPERTY_MAP: ContextMapping[] = [
{ from: '.signal', to: '.mcpReq.signal' },
{ from: '.requestId', to: '.mcpReq.id' },
{ from: '._meta', to: '.mcpReq._meta' },
{ from: '.sendRequest', to: '.mcpReq.send' },
{ from: '.sendNotification', to: '.mcpReq.notify' },
{ from: '.authInfo', to: '.http?.authInfo' },
{ from: '.sessionId', to: '.sessionId' },
{ from: '.requestInfo', to: '.http?.req' },
{ from: '.closeSSEStream', to: '.http?.closeSSE' },
{ from: '.closeStandaloneSSEStream', to: '.http?.closeStandaloneSSE' },
{ from: '.taskStore', to: '.task?.store' },
{ from: '.taskId', to: '.task?.id' },
{ from: '.taskRequestedTtl', to: '.task?.requestedTtl' }
];

export const EXTRA_PARAM_NAME = 'extra';
export const CTX_PARAM_NAME = 'ctx';
128 changes: 128 additions & 0 deletions packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
export interface ImportMapping {
target: string;
status: 'moved' | 'removed' | 'renamed';
renamedSymbols?: Record<string, string>;
removalMessage?: string;
}

export const IMPORT_MAP: Record<string, ImportMapping> = {
'@modelcontextprotocol/sdk/client/index.js': {
target: '@modelcontextprotocol/client',
status: 'moved'
},
'@modelcontextprotocol/sdk/client/auth.js': {
target: '@modelcontextprotocol/client',
status: 'moved'
},
'@modelcontextprotocol/sdk/client/streamableHttp.js': {
target: '@modelcontextprotocol/client',
status: 'moved'
},
'@modelcontextprotocol/sdk/client/sse.js': {
target: '@modelcontextprotocol/client',
status: 'moved'
},
'@modelcontextprotocol/sdk/client/stdio.js': {
target: '@modelcontextprotocol/client',
status: 'moved'
},
'@modelcontextprotocol/sdk/client/websocket.js': {
target: '',
status: 'removed',
removalMessage: 'WebSocketClientTransport removed in v2. Use StreamableHTTPClientTransport or StdioClientTransport.'
},

'@modelcontextprotocol/sdk/server/mcp.js': {
target: '@modelcontextprotocol/server',
status: 'moved'
},
'@modelcontextprotocol/sdk/server/index.js': {
target: '@modelcontextprotocol/server',
status: 'moved'
},
'@modelcontextprotocol/sdk/server/stdio.js': {
target: '@modelcontextprotocol/server',
status: 'moved'
},
'@modelcontextprotocol/sdk/server/streamableHttp.js': {
target: '@modelcontextprotocol/node',
status: 'renamed',
renamedSymbols: {
StreamableHTTPServerTransport: 'NodeStreamableHTTPServerTransport'
}
},
'@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js': {
target: '@modelcontextprotocol/server',
status: 'moved'
},
'@modelcontextprotocol/sdk/server/sse.js': {
target: '',
status: 'removed',
removalMessage: 'SSE server transport removed in v2. Migrate to NodeStreamableHTTPServerTransport from @modelcontextprotocol/node.'
},
'@modelcontextprotocol/sdk/server/middleware.js': {
target: '@modelcontextprotocol/express',
status: 'moved'
},

'@modelcontextprotocol/sdk/server/auth/types.js': {
target: '',
status: 'removed',
removalMessage:
'Server auth removed in v2. AuthInfo is now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server.'
},
'@modelcontextprotocol/sdk/server/auth/provider.js': {
target: '',
status: 'removed',
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
},
'@modelcontextprotocol/sdk/server/auth/router.js': {
target: '',
status: 'removed',
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
},
'@modelcontextprotocol/sdk/server/auth/middleware.js': {
target: '',
status: 'removed',
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
},
'@modelcontextprotocol/sdk/server/auth/errors.js': {
target: '',
status: 'removed',
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
},

'@modelcontextprotocol/sdk/types.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},
'@modelcontextprotocol/sdk/shared/protocol.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},
'@modelcontextprotocol/sdk/shared/transport.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},
'@modelcontextprotocol/sdk/shared/uriTemplate.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},
'@modelcontextprotocol/sdk/shared/auth.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},
'@modelcontextprotocol/sdk/shared/stdio.js': {
target: 'RESOLVE_BY_CONTEXT',
status: 'moved'
},

'@modelcontextprotocol/sdk/inMemory.js': {
target: '@modelcontextprotocol/core',
status: 'moved'
}
};

export function isAuthImport(specifier: string): boolean {
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
}
Loading
Loading