Skip to content
Merged
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'
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ pnpm-lock.yaml
# Ignore generated files
src/spec.types.ts

# Batch test cloned repos and results
packages/codemod/batch-test/repos
packages/codemod/batch-test/results

# Quickstart examples uses 2-space indent to match ecosystem conventions
examples/client-quickstart/
examples/server-quickstart/
3 changes: 3 additions & 0 deletions packages/codemod/batch-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
repos/
results/
tarballs/
102 changes: 102 additions & 0 deletions packages/codemod/batch-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Codemod Batch Test

Tests the v1-to-v2 codemod against real-world repos to find bugs, missing transforms, and gaps.

## How it works

For each repo in `repos.json`, the batch test:

1. Clones the repo (or resets an existing clone)
2. Installs dependencies
3. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy
4. Runs the codemod using the programmatic API
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)
6. Re-installs dependencies
7. Re-runs the same checks
8. Writes structured JSON reports

Errors that appear in step 7 but not step 3 are codemod-introduced regressions.

## Usage

```bash
# Build all SDK packages first (tarballs need built dist/)
pnpm build:all

# Run the batch test
pnpm --filter @modelcontextprotocol/codemod batch-test

# Clean cloned repos, results, and tarballs
pnpm --filter @modelcontextprotocol/codemod batch-test:clean
```

## Output

Results are written to `batch-test/results/`:

- `summary.json` — overview across all repos: which passed, which failed, error counts
- `<repo-slug>/report.json` — per-repo detail: baseline vs post-codemod check results, codemod diagnostics, change counts

## Repo manifest (`repos.json`)

An array of repo entries. Each entry specifies a GitHub repo and one or more packages within it.

```json
{
"repo": "owner/repo-name",
"ref": "main",
"packages": [
{
"dir": "packages/mcp-server",
"sourceDir": "src",
"checks": {
"typecheck": "npx tsc --noEmit",
"build": "npm run build",
"test": "npm run test",
"lint": null
}
}
]
}
```

| Field | Required | Default | Description |
| ---------------------- | -------- | -------------------------------------- | ----------------------------------------------------------------- |
| `repo` | yes | — | GitHub `owner/name` |
| `ref` | no | `main` | Branch or tag to clone |
| `packages` | no | `[{ "dir": ".", "sourceDir": "src" }]` | Package targets within the repo |
| `packages[].dir` | yes | — | Path to package root (where `package.json` lives) |
| `packages[].sourceDir` | no | `src` | Source directory relative to `dir` (passed to codemod) |
| `packages[].checks` | no | auto-detect | Override check commands; set a value to `null` to skip that check |

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.

## Analyzing results

`analyze-prompt.md` contains instructions for Claude Code to run the batch test and produce a categorized analysis. Each error is classified as:

| Category | Meaning |
| ------------------- | -------------------------------------------------- |
| `codemod-bug` | A transform produced incorrect output |
| `missing-transform` | The codemod should handle this pattern but doesn't |
| `manual-migration` | Expected — requires human judgment |
| `repo-specific` | Unusual pattern not worth handling in the codemod |

## Adding a repo

1. Edit `repos.json` and add an entry
2. Run `pnpm --filter @modelcontextprotocol/codemod batch-test`
3. Check `results/<repo-slug>/report.json` for new findings

For monorepos, list each package that uses `@modelcontextprotocol/sdk` as a separate entry in `packages`.

## Iteration workflow

```
1. Run the batch test
2. Review results — identify codemod bugs / missing transforms
3. Fix the codemod transforms
4. Run batch-test:clean, then re-run the batch test
5. Confirm the fixes resolved the issues
6. Repeat
```
87 changes: 87 additions & 0 deletions packages/codemod/batch-test/analyze-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Codemod Batch Test: Analysis

## Context

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
new import paths.

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
also updates `package.json` to swap v1 deps for v2.

The **batch test** runs this codemod against a curated list of real-world repos that use the v1 SDK. For each repo it:

1. Clones and installs
2. Runs baseline checks (typecheck, build, test, lint) to confirm the repo is healthy before migration
3. Runs the codemod
4. Re-installs (package.json was updated with v2 deps)
5. Re-runs the same checks

The goal is to find issues in the codemod itself — incorrect transforms, missing transforms, or gaps — so we can fix them.

## Instructions

1. Build the codemod:

```
pnpm --filter @modelcontextprotocol/codemod build
```

2. Run the batch test:

```
pnpm --filter @modelcontextprotocol/codemod batch-test
```

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.

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.

5. Also review the `codemod.diagnostics` array in each report — these are warnings the codemod itself emitted about patterns it couldn't fully handle.

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.

7. Categorize each finding using the categories below, then produce the output described in the Output Format section.

## Error Categories

| Category | Meaning | What to do |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `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. |
| `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. |
| `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. |

## Output Format

### Summary

- Repos tested: X
- Repos clean after codemod: Y
- Repos with new errors: Z
- Total codemod-introduced errors: N

### Findings by Category

#### Codemod Bugs

| Repo | File:Line | Error | Transform | Root Cause | Correct Output |
| ---- | --------- | ----- | --------- | ---------- | -------------- |

#### Missing Transforms

| Repo | File:Line | Error | v1 Pattern | Suggested Fix |
| ---- | --------- | ----- | ---------- | ------------- |

#### Manual Migration (expected)

| Repo | File:Line | Error | Has Diagnostic? | Migration Guide Reference |
| ---- | --------- | ----- | --------------- | ------------------------- |

#### Repo-Specific

| Repo | File:Line | Error | Why Not Worth Handling |
| ---- | --------- | ----- | ---------------------- |

### Priority Fixes

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.
44 changes: 44 additions & 0 deletions packages/codemod/batch-test/repos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"repo": "KKonstantinov/mcp-servers-fork",
"ref": "feature/upgrade-zod-v4",
"packages": [
{
"dir": "src/everything",
"sourceDir": ".",
"checks": {
"typecheck": "npx tsc --noEmit",
"build": "npm run build",
"test": "npm run test",
"lint": "npm run prettier:check"
}
}
]
},
{
"repo": "modelcontextprotocol/inspector",
"ref": "main",
"packages": [
{
"dir": "client",
"sourceDir": "src",
"checks": {
"typecheck": "npx tsc --noEmit",
"build": "npm run build",
"test": "npm run test",
"lint": "npm run lint"
}
},
{
"dir": "server",
"sourceDir": "src",
"checks": {
"typecheck": "npx tsc --noEmit",
"build": "npm run build",
"test": null,
"lint": null
}
}
]
}
]
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];
71 changes: 71 additions & 0 deletions packages/codemod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"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"
],
"scripts": {
"typecheck": "tsgo -p tsconfig.json --noEmit",
"generate:versions": "tsx scripts/generateVersions.ts",
"generate:spec-schemas": "tsx scripts/generateSpecSchemaMap.ts",
"prebuild": "pnpm run generate:versions && pnpm run generate:spec-schemas",
"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",
"batch-test": "tsx src/bin/batchTest.ts",
"batch-test:clean": "rm -rf batch-test/repos batch-test/results batch-test/tarballs",
"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"
}
}
39 changes: 39 additions & 0 deletions packages/codemod/scripts/generateSpecSchemaMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specTypeSchemaPath = path.resolve(__dirname, '../../core/src/types/specTypeSchema.ts');

const source = readFileSync(specTypeSchemaPath, 'utf8');

// Extract SPEC_SCHEMA_KEYS array entries
const keysMatch = source.match(/const SPEC_SCHEMA_KEYS = \[([\s\S]*?)\] as const/);
if (!keysMatch) throw new Error('Could not find SPEC_SCHEMA_KEYS in specTypeSchema.ts');

const protocolSchemas = [...keysMatch[1]!.matchAll(/'([^']+)'/g)].map(m => m[1]!);

// Extract auth schema keys
const authMatch = source.match(/const authSchemas = \{([\s\S]*?)\} as const/);
if (!authMatch) throw new Error('Could not find authSchemas in specTypeSchema.ts');

const authSchemas = [...authMatch[1]!.matchAll(/(\w+Schema)/g)].map(m => m[1]!);

const allSchemas = [...protocolSchemas, ...authSchemas].toSorted();

const entries = allSchemas.map((s, i) => ` '${s}'${i < allSchemas.length - 1 ? ',' : ''}`).join('\n');

const output = `// AUTO-GENERATED — do not edit. Run \`pnpm run generate:spec-schemas\` to regenerate.
export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
${entries}
]);

export function specSchemaToTypeName(schemaName: string): string | undefined {
if (!SPEC_SCHEMA_NAMES.has(schemaName)) return undefined;
return schemaName.slice(0, -'Schema'.length);
}
`;

const outPath = path.resolve(__dirname, '../src/generated/specSchemaMap.ts');
writeFileSync(outPath, output);
console.log(`Wrote ${outPath} (${allSchemas.length} schemas)`);
Loading
Loading