Skip to content

Commit 05eadea

Browse files
committed
feat: add v2 codemode draft
1 parent bdfd7f0 commit 05eadea

35 files changed

Lines changed: 3009 additions & 0 deletions

packages/codemod/eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// @ts-check
2+
3+
import baseConfig from '@modelcontextprotocol/eslint-config';
4+
5+
export default [...baseConfig];

packages/codemod/package.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"name": "@modelcontextprotocol/codemod",
3+
"version": "2.0.0-alpha.0",
4+
"description": "Codemod to migrate MCP TypeScript SDK code from v1 to v2",
5+
"license": "MIT",
6+
"author": "Anthropic, PBC (https://anthropic.com)",
7+
"homepage": "https://modelcontextprotocol.io",
8+
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
9+
"type": "module",
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
13+
},
14+
"engines": {
15+
"node": ">=20"
16+
},
17+
"keywords": [
18+
"modelcontextprotocol",
19+
"mcp",
20+
"codemod",
21+
"migration"
22+
],
23+
"bin": {
24+
"mcp-codemod": "./dist/cli.mjs"
25+
},
26+
"exports": {
27+
".": {
28+
"types": "./dist/index.d.mts",
29+
"import": "./dist/index.mjs"
30+
}
31+
},
32+
"files": [
33+
"dist"
34+
],
35+
"scripts": {
36+
"typecheck": "tsgo -p tsconfig.json --noEmit",
37+
"build": "tsdown",
38+
"build:watch": "tsdown --watch",
39+
"prepack": "pnpm run build",
40+
"lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .",
41+
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .",
42+
"check": "pnpm run typecheck && pnpm run lint",
43+
"test": "vitest run",
44+
"test:watch": "vitest"
45+
},
46+
"dependencies": {
47+
"commander": "^13.0.0",
48+
"ts-morph": "^28.0.0"
49+
},
50+
"devDependencies": {
51+
"@modelcontextprotocol/tsconfig": "workspace:^",
52+
"@modelcontextprotocol/vitest-config": "workspace:^",
53+
"@modelcontextprotocol/eslint-config": "workspace:^",
54+
"@eslint/js": "catalog:devTools",
55+
"@typescript/native-preview": "catalog:devTools",
56+
"eslint": "catalog:devTools",
57+
"eslint-config-prettier": "catalog:devTools",
58+
"eslint-plugin-n": "catalog:devTools",
59+
"prettier": "catalog:devTools",
60+
"tsdown": "catalog:devTools",
61+
"tsx": "catalog:devTools",
62+
"typescript": "catalog:devTools",
63+
"typescript-eslint": "catalog:devTools",
64+
"vitest": "catalog:devTools"
65+
}
66+
}

packages/codemod/src/cli.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env node
2+
3+
import { existsSync, statSync } from 'node:fs';
4+
import { createRequire } from 'node:module';
5+
import path from 'node:path';
6+
7+
import { Command } from 'commander';
8+
9+
import { listMigrations } from './migrations/index.js';
10+
import { run } from './runner.js';
11+
import { DiagnosticLevel } from './types.js';
12+
import { formatDiagnostic } from './utils/diagnostics.js';
13+
14+
const require = createRequire(import.meta.url);
15+
const { version } = require('../package.json') as { version: string };
16+
17+
const program = new Command();
18+
19+
program.name('mcp-codemod').description('Codemod to migrate MCP TypeScript SDK code between versions').version(version);
20+
21+
for (const [name, migration] of listMigrations()) {
22+
program
23+
.command(`${name} <target-dir>`)
24+
.description(migration.description)
25+
.option('-d, --dry-run', 'Preview changes without writing files')
26+
.option('-t, --transforms <ids>', 'Comma-separated transform IDs to run (default: all)')
27+
.option('-v, --verbose', 'Show detailed per-change output')
28+
.option('--ignore <patterns...>', 'Additional glob patterns to ignore')
29+
.option('--list', 'List available transforms for this migration')
30+
.action((targetDir: string, opts: Record<string, unknown>) => {
31+
try {
32+
if (opts['list']) {
33+
console.log(`\nAvailable transforms for ${name}:\n`);
34+
for (const t of migration.transforms) {
35+
console.log(` ${t.id.padEnd(20)} ${t.name}`);
36+
}
37+
console.log('');
38+
return;
39+
}
40+
41+
const resolvedDir = path.resolve(targetDir);
42+
43+
if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
44+
console.error(`\nError: "${resolvedDir}" is not a valid directory.\n`);
45+
process.exitCode = 1;
46+
return;
47+
}
48+
49+
console.log(`\n@modelcontextprotocol/codemod — ${migration.name}\n`);
50+
console.log(`Scanning ${resolvedDir}...`);
51+
if (opts['dryRun']) {
52+
console.log('(dry run — no files will be modified)\n');
53+
} else {
54+
console.log('');
55+
}
56+
57+
const transforms = opts['transforms'] ? (opts['transforms'] as string).split(',').map(s => s.trim()) : undefined;
58+
59+
const result = run(migration, {
60+
targetDir: resolvedDir,
61+
dryRun: opts['dryRun'] as boolean | undefined,
62+
verbose: opts['verbose'] as boolean | undefined,
63+
transforms,
64+
ignore: opts['ignore'] as string[] | undefined
65+
});
66+
67+
if (result.filesChanged === 0 && result.diagnostics.length === 0) {
68+
console.log('No changes needed — code already migrated or no SDK imports found.\n');
69+
return;
70+
}
71+
72+
if (result.filesChanged > 0) {
73+
console.log(`Changes: ${result.totalChanges} across ${result.filesChanged} file(s)\n`);
74+
}
75+
76+
if (opts['verbose']) {
77+
console.log('Files modified:');
78+
for (const fr of result.fileResults) {
79+
console.log(` ${fr.filePath} (${fr.changes} change(s))`);
80+
}
81+
console.log('');
82+
}
83+
84+
const errors = result.diagnostics.filter(d => d.level === DiagnosticLevel.Error);
85+
if (errors.length > 0) {
86+
console.log(`Errors (${errors.length}):`);
87+
for (const d of errors) {
88+
console.log(formatDiagnostic(d));
89+
}
90+
console.log('');
91+
process.exitCode = 1;
92+
}
93+
94+
const warnings = result.diagnostics.filter(d => d.level === DiagnosticLevel.Warning);
95+
if (warnings.length > 0) {
96+
console.log(`Warnings (${warnings.length}):`);
97+
for (const d of warnings) {
98+
console.log(formatDiagnostic(d));
99+
}
100+
console.log('');
101+
process.exitCode = 1;
102+
}
103+
104+
if (opts['dryRun']) {
105+
console.log('Run without --dry-run to apply changes.\n');
106+
} else {
107+
console.log('Migration complete. Review the changes and run your build/tests.\n');
108+
}
109+
} catch (error) {
110+
console.error(`\nError: ${error instanceof Error ? error.message : String(error)}\n`);
111+
process.exitCode = 1;
112+
}
113+
});
114+
}
115+
116+
program.parse();

packages/codemod/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export { getMigration, listMigrations } from './migrations/index.js';
2+
export { run } from './runner.js';
3+
export type {
4+
Diagnostic,
5+
FileResult,
6+
Migration,
7+
RunnerOptions,
8+
RunnerResult,
9+
Transform,
10+
TransformContext,
11+
TransformResult
12+
} from './types.js';
13+
export { DiagnosticLevel } from './types.js';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Migration } from '../types.js';
2+
import { v1ToV2Migration } from './v1-to-v2/index.js';
3+
4+
const migrations = new Map<string, Migration>([['v1-to-v2', v1ToV2Migration]]);
5+
6+
export function getMigration(name: string): Migration | undefined {
7+
return migrations.get(name);
8+
}
9+
10+
export function listMigrations(): Map<string, Migration> {
11+
return migrations;
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Migration } from '../../types.js';
2+
import { v1ToV2Transforms } from './transforms/index.js';
3+
4+
export const v1ToV2Migration: Migration = {
5+
name: 'v1-to-v2',
6+
description: 'Migrate from @modelcontextprotocol/sdk (v1) to v2 packages (@modelcontextprotocol/client, /server, etc.)',
7+
transforms: v1ToV2Transforms
8+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export interface ContextMapping {
2+
from: string;
3+
to: string;
4+
}
5+
6+
export const CONTEXT_PROPERTY_MAP: ContextMapping[] = [
7+
{ from: '.signal', to: '.mcpReq.signal' },
8+
{ from: '.requestId', to: '.mcpReq.id' },
9+
{ from: '._meta', to: '.mcpReq._meta' },
10+
{ from: '.sendRequest', to: '.mcpReq.send' },
11+
{ from: '.sendNotification', to: '.mcpReq.notify' },
12+
{ from: '.authInfo', to: '.http?.authInfo' },
13+
{ from: '.sessionId', to: '.sessionId' },
14+
{ from: '.requestInfo', to: '.http?.req' },
15+
{ from: '.closeSSEStream', to: '.http?.closeSSE' },
16+
{ from: '.closeStandaloneSSEStream', to: '.http?.closeStandaloneSSE' },
17+
{ from: '.taskStore', to: '.task?.store' },
18+
{ from: '.taskId', to: '.task?.id' },
19+
{ from: '.taskRequestedTtl', to: '.task?.requestedTtl' }
20+
];
21+
22+
export const EXTRA_PARAM_NAME = 'extra';
23+
export const CTX_PARAM_NAME = 'ctx';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
export interface ImportMapping {
2+
target: string;
3+
status: 'moved' | 'removed' | 'renamed';
4+
renamedSymbols?: Record<string, string>;
5+
removalMessage?: string;
6+
}
7+
8+
export const IMPORT_MAP: Record<string, ImportMapping> = {
9+
'@modelcontextprotocol/sdk/client/index.js': {
10+
target: '@modelcontextprotocol/client',
11+
status: 'moved'
12+
},
13+
'@modelcontextprotocol/sdk/client/auth.js': {
14+
target: '@modelcontextprotocol/client',
15+
status: 'moved'
16+
},
17+
'@modelcontextprotocol/sdk/client/streamableHttp.js': {
18+
target: '@modelcontextprotocol/client',
19+
status: 'moved'
20+
},
21+
'@modelcontextprotocol/sdk/client/sse.js': {
22+
target: '@modelcontextprotocol/client',
23+
status: 'moved'
24+
},
25+
'@modelcontextprotocol/sdk/client/stdio.js': {
26+
target: '@modelcontextprotocol/client',
27+
status: 'moved'
28+
},
29+
'@modelcontextprotocol/sdk/client/websocket.js': {
30+
target: '',
31+
status: 'removed',
32+
removalMessage: 'WebSocketClientTransport removed in v2. Use StreamableHTTPClientTransport or StdioClientTransport.'
33+
},
34+
35+
'@modelcontextprotocol/sdk/server/mcp.js': {
36+
target: '@modelcontextprotocol/server',
37+
status: 'moved'
38+
},
39+
'@modelcontextprotocol/sdk/server/index.js': {
40+
target: '@modelcontextprotocol/server',
41+
status: 'moved'
42+
},
43+
'@modelcontextprotocol/sdk/server/stdio.js': {
44+
target: '@modelcontextprotocol/server',
45+
status: 'moved'
46+
},
47+
'@modelcontextprotocol/sdk/server/streamableHttp.js': {
48+
target: '@modelcontextprotocol/node',
49+
status: 'renamed',
50+
renamedSymbols: {
51+
StreamableHTTPServerTransport: 'NodeStreamableHTTPServerTransport'
52+
}
53+
},
54+
'@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js': {
55+
target: '@modelcontextprotocol/server',
56+
status: 'moved'
57+
},
58+
'@modelcontextprotocol/sdk/server/sse.js': {
59+
target: '',
60+
status: 'removed',
61+
removalMessage: 'SSE server transport removed in v2. Migrate to NodeStreamableHTTPServerTransport from @modelcontextprotocol/node.'
62+
},
63+
'@modelcontextprotocol/sdk/server/middleware.js': {
64+
target: '@modelcontextprotocol/express',
65+
status: 'moved'
66+
},
67+
68+
'@modelcontextprotocol/sdk/server/auth/types.js': {
69+
target: '',
70+
status: 'removed',
71+
removalMessage:
72+
'Server auth removed in v2. AuthInfo is now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server.'
73+
},
74+
'@modelcontextprotocol/sdk/server/auth/provider.js': {
75+
target: '',
76+
status: 'removed',
77+
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
78+
},
79+
'@modelcontextprotocol/sdk/server/auth/router.js': {
80+
target: '',
81+
status: 'removed',
82+
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
83+
},
84+
'@modelcontextprotocol/sdk/server/auth/middleware.js': {
85+
target: '',
86+
status: 'removed',
87+
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
88+
},
89+
'@modelcontextprotocol/sdk/server/auth/errors.js': {
90+
target: '',
91+
status: 'removed',
92+
removalMessage: 'Server auth removed in v2. Use an external auth library (e.g., better-auth).'
93+
},
94+
95+
'@modelcontextprotocol/sdk/types.js': {
96+
target: 'RESOLVE_BY_CONTEXT',
97+
status: 'moved'
98+
},
99+
'@modelcontextprotocol/sdk/shared/protocol.js': {
100+
target: 'RESOLVE_BY_CONTEXT',
101+
status: 'moved'
102+
},
103+
'@modelcontextprotocol/sdk/shared/transport.js': {
104+
target: 'RESOLVE_BY_CONTEXT',
105+
status: 'moved'
106+
},
107+
'@modelcontextprotocol/sdk/shared/uriTemplate.js': {
108+
target: 'RESOLVE_BY_CONTEXT',
109+
status: 'moved'
110+
},
111+
'@modelcontextprotocol/sdk/shared/auth.js': {
112+
target: 'RESOLVE_BY_CONTEXT',
113+
status: 'moved'
114+
},
115+
'@modelcontextprotocol/sdk/shared/stdio.js': {
116+
target: 'RESOLVE_BY_CONTEXT',
117+
status: 'moved'
118+
},
119+
120+
'@modelcontextprotocol/sdk/inMemory.js': {
121+
target: '@modelcontextprotocol/core',
122+
status: 'moved'
123+
}
124+
};
125+
126+
export function isAuthImport(specifier: string): boolean {
127+
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
128+
}

0 commit comments

Comments
 (0)