Skip to content

Commit 48251fe

Browse files
feat: add v2 codemod draft (#1950)
1 parent 4f226c1 commit 48251fe

59 files changed

Lines changed: 9239 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ jobs:
4040
- name: Publish preview packages
4141
run:
4242
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
43-
'./packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'
43+
'./packages/codemod' './packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ pnpm-lock.yaml
1212
# Ignore generated files
1313
src/spec.types.ts
1414

15+
# Batch test cloned repos and results
16+
packages/codemod/batch-test/repos
17+
packages/codemod/batch-test/results
18+
1519
# Quickstart examples uses 2-space indent to match ecosystem conventions
1620
examples/client-quickstart/
1721
examples/server-quickstart/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
repos/
2+
results/
3+
tarballs/
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Codemod Batch Test
2+
3+
Tests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.
4+
5+
## How it works
6+
7+
For each repo in `repos.json`, the batch test:
8+
9+
1. Clones the repo (or resets an existing clone)
10+
2. Installs dependencies
11+
3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy
12+
4. Runs the codemod using the programmatic API
13+
5. Packs local SDK packages as tarballs and rewrites `package.json` deps to use them (so the test runs against the current SDK branch, not published npm versions)
14+
6. Re-installs dependencies
15+
7. Re-runs the same checks
16+
8. Writes structured JSON reports
17+
18+
Errors that appear in step 7 but not step 3 are codemod-introduced regressions.
19+
20+
## Usage
21+
22+
```bash
23+
# Build all SDK packages first (tarballs need built dist/)
24+
pnpm build:all
25+
26+
# Run the batch test
27+
pnpm --filter @modelcontextprotocol/codemod batch-test
28+
29+
# Clean cloned repos, results, and tarballs
30+
pnpm --filter @modelcontextprotocol/codemod batch-test:clean
31+
```
32+
33+
## Output
34+
35+
Results are written to `batch-test/results/`:
36+
37+
- `summary.json` — overview across all repos: which passed, which failed, error counts
38+
- `<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts
39+
40+
## Repo manifest (`repos.json`)
41+
42+
An array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.
43+
44+
```json
45+
{
46+
"repo": "owner/repo-name",
47+
"ref": "main",
48+
"packages": [
49+
{
50+
"dir": "packages/mcp-server",
51+
"sourceDir": "src",
52+
"checks": {
53+
"typecheck": "npx tsc --noEmit",
54+
"build": "npm run build",
55+
"test": "npm run test",
56+
"lint": null
57+
}
58+
}
59+
]
60+
}
61+
```
62+
63+
| Field | Required | Default | Description |
64+
| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |
65+
| `repo` | yes || GitHub `owner/name` |
66+
| `ref` | no | `main` | Branch or tag to clone |
67+
| `packages` | no | `[{ "dir": ".", "sourceDir": "src" }]` | Package targets within the repo |
68+
| `packages[].dir` | yes || Path to package root (where `package.json` lives) |
69+
| `packages[].sourceDir` | no | `src` | Source directory relative to `dir` (passed to codemod) |
70+
| `packages[].checks` | no | auto-detect | Override check commands; set a value to `null` to skip that check |
71+
72+
When `checks` is omitted, the runner auto-detects commands from the package's `package.json` scripts (probing names like `typecheck`, `build`, `test`, `lint`). The package manager is auto-detected from the lockfile at the repo root.
73+
74+
## Analyzing results
75+
76+
`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:
77+
78+
| Category | Meaning |
79+
| ------------------- | -------------------------------------------------- |
80+
| `codemod-bug` | A transform produced incorrect output |
81+
| `missing-transform` | The codemod should handle this pattern but doesn't |
82+
| `manual-migration` | Expected — requires human judgment |
83+
| `repo-specific` | Unusual pattern not worth handling in the codemod |
84+
85+
## Adding a repo
86+
87+
1. Edit `repos.json` and add an entry
88+
2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`
89+
3. Check `results/<repo-slug>/report.json` for new findings
90+
91+
For monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.
92+
93+
## Iteration workflow
94+
95+
```
96+
1. Run the batch test
97+
2. Review results — identify codemod bugs / missing transforms
98+
3. Fix the codemod transforms
99+
4. Run batch-test:clean, then re-run the batch test
100+
5. Confirm the fixes resolved the issues
101+
6. Repeat
102+
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Codemod Batch Test: Analysis
2+
3+
## Context
4+
5+
The MCP TypeScript SDK is migrating from a single v1 package (`@modelcontextprotocol/sdk`) to a multi-package v2 architecture (`@modelcontextprotocol/client`, `/server`, `/core`, `/node`, `/express`). This involves renamed APIs, restructured context objects, removed modules, and
6+
new import paths.
7+
8+
The `@modelcontextprotocol/codemod` package automates the mechanical parts of this migration. It runs 9 ordered AST transforms via ts-morph: import path rewrites, symbol renames, McpServer API restructuring, handler registration changes, context property remapping, and more. It
9+
also updates `package.json` to swap v1 deps for v2.
10+
11+
The **batch test** runs this codemod against a curated list of real-world repos that use the v1 SDK. For each repo it:
12+
13+
1. Clones and installs
14+
2. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy before migration
15+
3. Runs the codemod
16+
4. Re-installs (package.json was updated with v2 deps)
17+
5. Re-runs the same checks
18+
19+
The goal is to find issues in the codemod itself — incorrect transforms, missing transforms, or gaps — so we can fix them.
20+
21+
## Instructions
22+
23+
1. Build the codemod:
24+
25+
```
26+
pnpm --filter @modelcontextprotocol/codemod build
27+
```
28+
29+
2. Run the batch test:
30+
31+
```
32+
pnpm --filter @modelcontextprotocol/codemod batch-test
33+
```
34+
35+
3. Read `packages/codemod/batch-test/results/summary.json` for the overview. Note which repos have `postCodemodClean: false` and which check types have new errors.
36+
37+
4. For each repo with new errors, read its `packages/codemod/batch-test/results/<repo-slug>/report.json`. Compare `baseline` vs `postCodemod` for each check — only errors that appear in `postCodemod` but not in `baseline` are codemod-introduced.
38+
39+
5. Also review the `codemod.diagnostics` array in each report — these are warnings the codemod itself emitted about patterns it couldn't fully handle.
40+
41+
6. For each codemod-introduced error, look at the actual source file in the cloned repo (`packages/codemod/batch-test/repos/<repo-slug>/...`) to understand what the codemod produced and what it should have produced.
42+
43+
7. Categorize each finding using the categories below, then produce the output described in the Output Format section.
44+
45+
## Error Categories
46+
47+
| Category | Meaning | What to do |
48+
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
49+
| `codemod-bug` | A transform produced incorrect output — the code it generated is wrong | Identify which transform is responsible and what the correct output should be. This is a bug to fix in the codemod. |
50+
| `missing-transform` | The codemod left v1 code untouched that it should have migrated | Identify the v1 pattern and which existing transform should handle it, or whether a new transform is needed. |
51+
| `manual-migration` | The error is expected — the migration guide documents this as requiring human judgment (e.g., removed APIs, architectural changes) | Verify the codemod emitted a diagnostic for it. If not, add one. |
52+
| `repo-specific` | An unusual pattern unique to this repo that isn't worth handling in the codemod | Note it briefly but don't suggest codemod changes. |
53+
54+
## Output Format
55+
56+
### Summary
57+
58+
- Repos tested: X
59+
- Repos clean after codemod: Y
60+
- Repos with new errors: Z
61+
- Total codemod-introduced errors: N
62+
63+
### Findings by Category
64+
65+
#### Codemod Bugs
66+
67+
| Repo | File:Line | Error | Transform | Root Cause | Correct Output |
68+
| ---- | --------- | ----- | --------- | ---------- | -------------- |
69+
70+
#### Missing Transforms
71+
72+
| Repo | File:Line | Error | v1 Pattern | Suggested Fix |
73+
| ---- | --------- | ----- | ---------- | ------------- |
74+
75+
#### Manual Migration (expected)
76+
77+
| Repo | File:Line | Error | Has Diagnostic? | Migration Guide Reference |
78+
| ---- | --------- | ----- | --------------- | ------------------------- |
79+
80+
#### Repo-Specific
81+
82+
| Repo | File:Line | Error | Why Not Worth Handling |
83+
| ---- | --------- | ----- | ---------------------- |
84+
85+
### Priority Fixes
86+
87+
List the top 3-5 codemod improvements that would fix the most repos, ordered by impact (number of repos affected). For each, state: what to change, in which transform, and how many repos it would fix.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"repo": "KKonstantinov/mcp-servers-fork",
4+
"ref": "feature/upgrade-zod-v4",
5+
"packages": [
6+
{
7+
"dir": "src/everything",
8+
"sourceDir": ".",
9+
"checks": {
10+
"typecheck": "npx tsc --noEmit",
11+
"build": "npm run build",
12+
"test": "npm run test",
13+
"lint": "npm run prettier:check"
14+
}
15+
}
16+
]
17+
},
18+
{
19+
"repo": "modelcontextprotocol/inspector",
20+
"ref": "main",
21+
"packages": [
22+
{
23+
"dir": "client",
24+
"sourceDir": "src",
25+
"checks": {
26+
"typecheck": "npx tsc --noEmit",
27+
"build": "npm run build",
28+
"test": "npm run test",
29+
"lint": "npm run lint"
30+
}
31+
},
32+
{
33+
"dir": "server",
34+
"sourceDir": "src",
35+
"checks": {
36+
"typecheck": "npx tsc --noEmit",
37+
"build": "npm run build",
38+
"test": null,
39+
"lint": null
40+
}
41+
}
42+
]
43+
}
44+
]

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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
"generate:versions": "tsx scripts/generateVersions.ts",
38+
"generate:spec-schemas": "tsx scripts/generateSpecSchemaMap.ts",
39+
"prebuild": "pnpm run generate:versions && pnpm run generate:spec-schemas",
40+
"build": "tsdown",
41+
"build:watch": "tsdown --watch",
42+
"prepack": "pnpm run build",
43+
"lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .",
44+
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .",
45+
"check": "pnpm run typecheck && pnpm run lint",
46+
"batch-test": "tsx src/bin/batchTest.ts",
47+
"batch-test:clean": "rm -rf batch-test/repos batch-test/results batch-test/tarballs",
48+
"test": "vitest run",
49+
"test:watch": "vitest"
50+
},
51+
"dependencies": {
52+
"commander": "^13.0.0",
53+
"ts-morph": "^28.0.0"
54+
},
55+
"devDependencies": {
56+
"@modelcontextprotocol/tsconfig": "workspace:^",
57+
"@modelcontextprotocol/vitest-config": "workspace:^",
58+
"@modelcontextprotocol/eslint-config": "workspace:^",
59+
"@eslint/js": "catalog:devTools",
60+
"@typescript/native-preview": "catalog:devTools",
61+
"eslint": "catalog:devTools",
62+
"eslint-config-prettier": "catalog:devTools",
63+
"eslint-plugin-n": "catalog:devTools",
64+
"prettier": "catalog:devTools",
65+
"tsdown": "catalog:devTools",
66+
"tsx": "catalog:devTools",
67+
"typescript": "catalog:devTools",
68+
"typescript-eslint": "catalog:devTools",
69+
"vitest": "catalog:devTools"
70+
}
71+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { readFileSync, writeFileSync } from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const specTypeSchemaPath = path.resolve(__dirname, '../../core/src/types/specTypeSchema.ts');
7+
8+
const source = readFileSync(specTypeSchemaPath, 'utf8');
9+
10+
// Extract SPEC_SCHEMA_KEYS array entries
11+
const keysMatch = source.match(/const SPEC_SCHEMA_KEYS = \[([\s\S]*?)\] as const/);
12+
if (!keysMatch) throw new Error('Could not find SPEC_SCHEMA_KEYS in specTypeSchema.ts');
13+
14+
const protocolSchemas = [...keysMatch[1]!.matchAll(/'([^']+)'/g)].map(m => m[1]!);
15+
16+
// Extract auth schema keys
17+
const authMatch = source.match(/const authSchemas = \{([\s\S]*?)\} as const/);
18+
if (!authMatch) throw new Error('Could not find authSchemas in specTypeSchema.ts');
19+
20+
const authSchemas = [...authMatch[1]!.matchAll(/(\w+Schema)/g)].map(m => m[1]!);
21+
22+
const allSchemas = [...protocolSchemas, ...authSchemas].toSorted();
23+
24+
const entries = allSchemas.map((s, i) => ` '${s}'${i < allSchemas.length - 1 ? ',' : ''}`).join('\n');
25+
26+
const output = `// AUTO-GENERATED — do not edit. Run \`pnpm run generate:spec-schemas\` to regenerate.
27+
export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
28+
${entries}
29+
]);
30+
31+
export function specSchemaToTypeName(schemaName: string): string | undefined {
32+
if (!SPEC_SCHEMA_NAMES.has(schemaName)) return undefined;
33+
return schemaName.slice(0, -'Schema'.length);
34+
}
35+
`;
36+
37+
const outPath = path.resolve(__dirname, '../src/generated/specSchemaMap.ts');
38+
writeFileSync(outPath, output);
39+
console.log(`Wrote ${outPath} (${allSchemas.length} schemas)`);

0 commit comments

Comments
 (0)