feat: add v2 codemod draft (#1950)
This commit is contained in:
parent
4f226c1e35
commit
48251fe2cb
59 changed files with 9239 additions and 1 deletions
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
packages/codemod/batch-test/.gitignore
vendored
Normal file
3
packages/codemod/batch-test/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
repos/
|
||||
results/
|
||||
tarballs/
|
||||
102
packages/codemod/batch-test/README.md
Normal file
102
packages/codemod/batch-test/README.md
Normal file
|
|
@ -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
packages/codemod/batch-test/analyze-prompt.md
Normal file
87
packages/codemod/batch-test/analyze-prompt.md
Normal file
|
|
@ -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
packages/codemod/batch-test/repos.json
Normal file
44
packages/codemod/batch-test/repos.json
Normal file
|
|
@ -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
packages/codemod/eslint.config.mjs
Normal file
5
packages/codemod/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
import baseConfig from '@modelcontextprotocol/eslint-config';
|
||||
|
||||
export default [...baseConfig];
|
||||
71
packages/codemod/package.json
Normal file
71
packages/codemod/package.json
Normal file
|
|
@ -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
packages/codemod/scripts/generateSpecSchemaMap.ts
Normal file
39
packages/codemod/scripts/generateSpecSchemaMap.ts
Normal file
|
|
@ -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)`);
|
||||
34
packages/codemod/scripts/generateVersions.ts
Normal file
34
packages/codemod/scripts/generateVersions.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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 packagesDir = path.resolve(__dirname, '../..');
|
||||
|
||||
const PACKAGE_DIRS: Record<string, string> = {
|
||||
'@modelcontextprotocol/client': 'client',
|
||||
'@modelcontextprotocol/server': 'server',
|
||||
'@modelcontextprotocol/node': 'middleware/node',
|
||||
'@modelcontextprotocol/express': 'middleware/express'
|
||||
};
|
||||
|
||||
const versions: Record<string, string> = {};
|
||||
|
||||
for (const [pkg, dir] of Object.entries(PACKAGE_DIRS)) {
|
||||
const pkgJsonPath = path.join(packagesDir, dir, 'package.json');
|
||||
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||
versions[pkg] = `^${pkgJson.version}`;
|
||||
}
|
||||
|
||||
const entries = Object.entries(versions);
|
||||
const lines = entries.map(([pkg, ver], i) => ` '${pkg}': '${ver}'${i < entries.length - 1 ? ',' : ''}`).join('\n');
|
||||
|
||||
const output = `// AUTO-GENERATED — do not edit. Run \`pnpm run generate:versions\` to regenerate.
|
||||
export const V2_PACKAGE_VERSIONS: Record<string, string> = {
|
||||
${lines}
|
||||
};
|
||||
`;
|
||||
|
||||
const outPath = path.resolve(__dirname, '../src/generated/versions.ts');
|
||||
writeFileSync(outPath, output);
|
||||
console.log(`Wrote ${outPath}`);
|
||||
463
packages/codemod/src/bin/batchTest.ts
Normal file
463
packages/codemod/src/bin/batchTest.ts
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { getMigration } from '../migrations/index.js';
|
||||
import { run } from '../runner.js';
|
||||
import type { Diagnostic, RunnerResult } from '../types.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface PackageEntry {
|
||||
dir: string;
|
||||
sourceDir?: string;
|
||||
checks?: Record<string, string | null>;
|
||||
}
|
||||
|
||||
interface RepoEntry {
|
||||
repo: string;
|
||||
ref?: string;
|
||||
packages?: PackageEntry[];
|
||||
}
|
||||
|
||||
interface CheckResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
interface PackageReport {
|
||||
dir: string;
|
||||
sourceDir: string;
|
||||
codemod: {
|
||||
filesChanged: number;
|
||||
totalChanges: number;
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
baseline: Record<string, CheckResult>;
|
||||
postCodemod: Record<string, CheckResult>;
|
||||
}
|
||||
|
||||
interface RepoReport {
|
||||
repo: string;
|
||||
ref: string;
|
||||
timestamp: string;
|
||||
packageManager: string;
|
||||
packages: PackageReport[];
|
||||
}
|
||||
|
||||
interface SummaryEntry {
|
||||
repo: string;
|
||||
package: string;
|
||||
baselineClean: boolean;
|
||||
postCodemodClean: boolean;
|
||||
newErrors: Record<string, number>;
|
||||
codemodDiagnostics: Record<string, number>;
|
||||
}
|
||||
|
||||
interface Summary {
|
||||
timestamp: string;
|
||||
codemodVersion: string;
|
||||
codemodCommit: string;
|
||||
totalRepos: number;
|
||||
totalPackages: number;
|
||||
results: SummaryEntry[];
|
||||
aggregated: {
|
||||
reposClean: number;
|
||||
reposWithNewErrors: number;
|
||||
totalNewTypecheckErrors: number;
|
||||
totalCodemodWarnings: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SDK_ROOT = path.resolve(SCRIPT_DIR, '../../../..');
|
||||
const BATCH_DIR = path.resolve(SDK_ROOT, 'packages/codemod/batch-test');
|
||||
|
||||
const LOCAL_PACKAGE_DIRS: Record<string, string> = {
|
||||
'@modelcontextprotocol/client': path.join(SDK_ROOT, 'packages/client'),
|
||||
'@modelcontextprotocol/core': path.join(SDK_ROOT, 'packages/core'),
|
||||
'@modelcontextprotocol/server': path.join(SDK_ROOT, 'packages/server'),
|
||||
'@modelcontextprotocol/express': path.join(SDK_ROOT, 'packages/middleware/express'),
|
||||
'@modelcontextprotocol/fastify': path.join(SDK_ROOT, 'packages/middleware/fastify'),
|
||||
'@modelcontextprotocol/hono': path.join(SDK_ROOT, 'packages/middleware/hono'),
|
||||
'@modelcontextprotocol/node': path.join(SDK_ROOT, 'packages/middleware/node')
|
||||
};
|
||||
|
||||
const TARBALL_DIR = path.join(BATCH_DIR, 'tarballs');
|
||||
|
||||
const CHECK_SCRIPT_NAMES: Record<string, string[]> = {
|
||||
typecheck: ['typecheck', 'type-check', 'check:types', 'tsc'],
|
||||
build: ['build', 'compile'],
|
||||
test: ['test', 'test:unit', 'test:all'],
|
||||
lint: ['lint', 'lint:check']
|
||||
};
|
||||
|
||||
function detectPm(repoRoot: string): string {
|
||||
if (existsSync(path.join(repoRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
||||
if (existsSync(path.join(repoRoot, 'yarn.lock'))) return 'yarn';
|
||||
if (existsSync(path.join(repoRoot, 'bun.lockb'))) return 'bun';
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
function detectCheckCmd(pkgDir: string, checkType: string): string | null {
|
||||
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
||||
if (!existsSync(pkgJsonPath)) return null;
|
||||
|
||||
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as { scripts?: Record<string, string> };
|
||||
const scripts = pkgJson.scripts ?? {};
|
||||
const candidates = CHECK_SCRIPT_NAMES[checkType] ?? [];
|
||||
|
||||
for (const name of candidates) {
|
||||
if (name in scripts) return name;
|
||||
}
|
||||
|
||||
if (checkType === 'typecheck') return '__fallback_tsc';
|
||||
return null;
|
||||
}
|
||||
|
||||
function shell(cmd: string, cwd?: string): { exitCode: number; stdout: string; stderr: string } {
|
||||
try {
|
||||
const stdout = execSync(cmd, {
|
||||
cwd,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: 5 * 60 * 1000
|
||||
}).toString();
|
||||
return { exitCode: 0, stdout, stderr: '' };
|
||||
} catch (error: unknown) {
|
||||
const e = error as { status?: number; stdout?: Buffer; stderr?: Buffer };
|
||||
return {
|
||||
exitCode: e.status ?? 1,
|
||||
stdout: e.stdout?.toString() ?? '',
|
||||
stderr: e.stderr?.toString() ?? ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function runCheck(pm: string, pkgDir: string, checkType: string, override?: string | null): CheckResult {
|
||||
if (override === null) {
|
||||
return { exitCode: -1, stdout: '', stderr: 'skipped by manifest' };
|
||||
}
|
||||
|
||||
let cmd: string;
|
||||
if (override) {
|
||||
cmd = override;
|
||||
} else {
|
||||
const detected = detectCheckCmd(pkgDir, checkType);
|
||||
if (!detected) {
|
||||
return { exitCode: -1, stdout: '', stderr: 'skipped: no matching script' };
|
||||
}
|
||||
cmd = detected === '__fallback_tsc' ? 'npx tsc --noEmit' : `${pm} run ${detected}`;
|
||||
}
|
||||
|
||||
return shell(cmd, pkgDir);
|
||||
}
|
||||
|
||||
function truncate(s: string, max = 50_000): string {
|
||||
return s.length > max ? s.slice(0, max) + '\n... (truncated)' : s;
|
||||
}
|
||||
|
||||
function packLocalPackages(): Record<string, string> {
|
||||
mkdirSync(TARBALL_DIR, { recursive: true });
|
||||
|
||||
const tarballs: Record<string, string> = {};
|
||||
for (const [name, pkgDir] of Object.entries(LOCAL_PACKAGE_DIRS)) {
|
||||
console.log(` Packing ${name}...`);
|
||||
const result = shell(`pnpm pack --pack-destination ${JSON.stringify(TARBALL_DIR)}`, pkgDir);
|
||||
if (result.exitCode !== 0) {
|
||||
console.error(` ERROR: failed to pack ${name}: ${result.stderr.split('\n')[0]}`);
|
||||
continue;
|
||||
}
|
||||
const tarballFile = result.stdout.trim().split('\n').pop()!;
|
||||
tarballs[name] = path.resolve(TARBALL_DIR, path.basename(tarballFile));
|
||||
}
|
||||
return tarballs;
|
||||
}
|
||||
|
||||
function rewriteToLocalTarballs(pkgJsonPath: string, tarballs: Record<string, string>): number {
|
||||
const raw = readFileSync(pkgJsonPath, 'utf8');
|
||||
const pkgJson = JSON.parse(raw) as Record<string, unknown>;
|
||||
let rewrites = 0;
|
||||
|
||||
for (const section of ['dependencies', 'devDependencies']) {
|
||||
const deps = pkgJson[section] as Record<string, string> | undefined;
|
||||
if (!deps) continue;
|
||||
for (const [name, tarballPath] of Object.entries(tarballs)) {
|
||||
if (name in deps) {
|
||||
deps[name] = `file:${tarballPath}`;
|
||||
rewrites++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rewrites > 0) {
|
||||
const indent = raw.match(/^(\s+)"/m)?.[1] ?? ' ';
|
||||
const trailingNewline = raw.endsWith('\n');
|
||||
let output = JSON.stringify(pkgJson, null, indent);
|
||||
if (trailingNewline) output += '\n';
|
||||
writeFileSync(pkgJsonPath, output);
|
||||
}
|
||||
|
||||
return rewrites;
|
||||
}
|
||||
|
||||
function getCheckOverride(checks: Record<string, string | null>, type: string): string | null | undefined {
|
||||
if (type in checks) return checks[type] ?? null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isAllClean(checks: Record<string, CheckResult>): boolean {
|
||||
return Object.values(checks).every(c => c.exitCode === 0 || c.exitCode === -1);
|
||||
}
|
||||
|
||||
function hasNewError(baseline: Record<string, CheckResult>, post: Record<string, CheckResult>, type: string): number {
|
||||
return baseline[type]!.exitCode === 0 && post[type]!.exitCode !== 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CLONE_DIR = path.join(BATCH_DIR, 'repos');
|
||||
const OUTPUT_DIR = path.join(BATCH_DIR, 'results');
|
||||
|
||||
function parseArgs(): { manifest: string } {
|
||||
const args = process.argv.slice(2).filter(a => a !== '--');
|
||||
const opts = {
|
||||
manifest: path.join(BATCH_DIR, 'repos.json')
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--manifest': {
|
||||
opts.manifest = args[++i]!;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(`Unknown flag: ${args[i]}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const opts = parseArgs();
|
||||
|
||||
if (!existsSync(opts.manifest)) {
|
||||
console.error(`Error: manifest not found at ${opts.manifest}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const migration = getMigration('v1-to-v2');
|
||||
if (!migration) {
|
||||
console.error('Error: v1-to-v2 migration not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(CLONE_DIR, { recursive: true });
|
||||
mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const manifest: RepoEntry[] = JSON.parse(readFileSync(opts.manifest, 'utf8')) as RepoEntry[];
|
||||
const codemodPkg = JSON.parse(readFileSync(path.join(SDK_ROOT, 'packages/codemod/package.json'), 'utf8')) as { version: string };
|
||||
const codemodVersion = codemodPkg.version;
|
||||
const codemodCommit = execSync('git rev-parse --short HEAD', { cwd: SDK_ROOT }).toString().trim();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
console.log('=== Codemod Batch Test ===');
|
||||
console.log(`Manifest: ${opts.manifest} (${manifest.length} repos)`);
|
||||
console.log(`Codemod: v${codemodVersion} (${codemodCommit})`);
|
||||
console.log('');
|
||||
|
||||
console.log('--- Packing local SDK packages ---');
|
||||
const tarballs = packLocalPackages();
|
||||
console.log(` Packed ${Object.keys(tarballs).length} packages\n`);
|
||||
|
||||
const summaryResults: SummaryEntry[] = [];
|
||||
let totalPackages = 0;
|
||||
|
||||
for (let i = 0; i < manifest.length; i++) {
|
||||
const entry = manifest[i]!;
|
||||
const ref = entry.ref ?? 'main';
|
||||
const repoSlug = entry.repo.replace('/', '_');
|
||||
const clonePath = path.join(CLONE_DIR, repoSlug);
|
||||
|
||||
console.log(`--- [${i + 1}/${manifest.length}] ${entry.repo} (${ref}) ---`);
|
||||
|
||||
// Step 1: Clone or reset
|
||||
if (existsSync(path.join(clonePath, '.git'))) {
|
||||
console.log(' Resetting existing clone...');
|
||||
shell('git restore .', clonePath);
|
||||
shell('git clean -fd', clonePath);
|
||||
} else {
|
||||
console.log(' Cloning...');
|
||||
const cloneResult = shell(
|
||||
`git clone --depth 1 --branch ${ref} https://github.com/${entry.repo}.git ${JSON.stringify(clonePath)}`
|
||||
);
|
||||
if (cloneResult.exitCode !== 0) {
|
||||
console.log(` ERROR: clone failed, skipping\n ${cloneResult.stderr.split('\n')[0]}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Detect package manager
|
||||
const pm = detectPm(clonePath);
|
||||
console.log(` Package manager: ${pm}`);
|
||||
|
||||
// Step 3: Install
|
||||
console.log(' Installing dependencies...');
|
||||
const installResult = shell(`${pm} install --ignore-scripts`, clonePath);
|
||||
if (installResult.exitCode !== 0) {
|
||||
console.log(` ERROR: install failed, skipping\n ${installResult.stderr.split('\n')[0]}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process packages
|
||||
const packages: PackageEntry[] = entry.packages ?? [{ dir: '.', sourceDir: 'src' }];
|
||||
const repoPkgResults: PackageReport[] = [];
|
||||
|
||||
for (const pkg of packages) {
|
||||
const sourceDir = pkg.sourceDir ?? 'src';
|
||||
const fullPkgDir = path.join(clonePath, pkg.dir);
|
||||
const fullSourceDir = path.join(fullPkgDir, sourceDir);
|
||||
|
||||
console.log(` Package: ${pkg.dir} (source: ${sourceDir})`);
|
||||
|
||||
const checks = pkg.checks ?? {};
|
||||
|
||||
// Step 4: Baseline checks
|
||||
console.log(' Running baseline checks...');
|
||||
const baseline: Record<string, CheckResult> = {};
|
||||
for (const checkType of ['typecheck', 'build', 'test', 'lint']) {
|
||||
baseline[checkType] = runCheck(pm, fullPkgDir, checkType, getCheckOverride(checks, checkType));
|
||||
}
|
||||
console.log(
|
||||
` Baseline: tc=${baseline['typecheck']!.exitCode} build=${baseline['build']!.exitCode} test=${baseline['test']!.exitCode} lint=${baseline['lint']!.exitCode}`
|
||||
);
|
||||
|
||||
// Step 5: Run codemod (programmatic API)
|
||||
console.log(' Running codemod...');
|
||||
let codemodResult: RunnerResult;
|
||||
try {
|
||||
codemodResult = run(migration, { targetDir: fullSourceDir, verbose: true });
|
||||
} catch (error) {
|
||||
console.log(` ERROR: codemod threw: ${error}`);
|
||||
codemodResult = { filesChanged: 0, totalChanges: 0, diagnostics: [], fileResults: [] };
|
||||
}
|
||||
console.log(
|
||||
` Codemod: files=${codemodResult.filesChanged} changes=${codemodResult.totalChanges} diags=${codemodResult.diagnostics.length}`
|
||||
);
|
||||
|
||||
// Step 6: Rewrite v2 deps to local tarballs, then re-install
|
||||
const rewrites = rewriteToLocalTarballs(path.join(fullPkgDir, 'package.json'), tarballs);
|
||||
if (rewrites > 0) {
|
||||
console.log(` Rewrote ${rewrites} deps to local tarballs`);
|
||||
}
|
||||
console.log(' Re-installing dependencies...');
|
||||
shell(`${pm} install --ignore-scripts`, clonePath);
|
||||
|
||||
// Step 7: Post-codemod checks
|
||||
console.log(' Running post-codemod checks...');
|
||||
const postCodemod: Record<string, CheckResult> = {};
|
||||
for (const checkType of ['typecheck', 'build', 'test', 'lint']) {
|
||||
postCodemod[checkType] = runCheck(pm, fullPkgDir, checkType, getCheckOverride(checks, checkType));
|
||||
}
|
||||
console.log(
|
||||
` Post: tc=${postCodemod['typecheck']!.exitCode} build=${postCodemod['build']!.exitCode} test=${postCodemod['test']!.exitCode} lint=${postCodemod['lint']!.exitCode}`
|
||||
);
|
||||
|
||||
// Truncate large outputs for the report
|
||||
for (const r of [...Object.values(baseline), ...Object.values(postCodemod)]) {
|
||||
r.stdout = truncate(r.stdout);
|
||||
r.stderr = truncate(r.stderr);
|
||||
}
|
||||
|
||||
repoPkgResults.push({
|
||||
dir: pkg.dir,
|
||||
sourceDir,
|
||||
codemod: {
|
||||
filesChanged: codemodResult.filesChanged,
|
||||
totalChanges: codemodResult.totalChanges,
|
||||
diagnostics: codemodResult.diagnostics
|
||||
},
|
||||
baseline,
|
||||
postCodemod
|
||||
});
|
||||
totalPackages++;
|
||||
|
||||
summaryResults.push({
|
||||
repo: entry.repo,
|
||||
package: pkg.dir,
|
||||
baselineClean: isAllClean(baseline),
|
||||
postCodemodClean: isAllClean(postCodemod),
|
||||
newErrors: {
|
||||
typecheck: hasNewError(baseline, postCodemod, 'typecheck'),
|
||||
build: hasNewError(baseline, postCodemod, 'build'),
|
||||
test: hasNewError(baseline, postCodemod, 'test'),
|
||||
lint: hasNewError(baseline, postCodemod, 'lint')
|
||||
},
|
||||
codemodDiagnostics: {
|
||||
warning: codemodResult.diagnostics.filter(d => d.level === 'warning').length,
|
||||
error: codemodResult.diagnostics.filter(d => d.level === 'error').length,
|
||||
info: codemodResult.diagnostics.filter(d => d.level === 'info').length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Step 8: Write per-repo report
|
||||
const repoOutputDir = path.join(OUTPUT_DIR, repoSlug);
|
||||
mkdirSync(repoOutputDir, { recursive: true });
|
||||
|
||||
const report: RepoReport = {
|
||||
repo: entry.repo,
|
||||
ref,
|
||||
timestamp,
|
||||
packageManager: pm,
|
||||
packages: repoPkgResults
|
||||
};
|
||||
writeFileSync(path.join(repoOutputDir, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(` Report: ${repoOutputDir}/report.json\n`);
|
||||
}
|
||||
|
||||
// Write summary
|
||||
const reposClean = summaryResults.filter(r => r.postCodemodClean).length;
|
||||
const reposWithErrors = summaryResults.filter(r => !r.postCodemodClean).length;
|
||||
const totalNewTc = summaryResults.reduce((sum, r) => sum + r.newErrors['typecheck']!, 0);
|
||||
const totalWarnings = summaryResults.reduce((sum, r) => sum + r.codemodDiagnostics['warning']!, 0);
|
||||
|
||||
const summary: Summary = {
|
||||
timestamp,
|
||||
codemodVersion,
|
||||
codemodCommit,
|
||||
totalRepos: manifest.length,
|
||||
totalPackages,
|
||||
results: summaryResults,
|
||||
aggregated: {
|
||||
reposClean,
|
||||
reposWithNewErrors: reposWithErrors,
|
||||
totalNewTypecheckErrors: totalNewTc,
|
||||
totalCodemodWarnings: totalWarnings
|
||||
}
|
||||
};
|
||||
writeFileSync(path.join(OUTPUT_DIR, 'summary.json'), JSON.stringify(summary, null, 2));
|
||||
|
||||
console.log('=== Summary ===');
|
||||
console.log(`Repos: ${manifest.length} | Packages: ${totalPackages}`);
|
||||
console.log(`Clean after codemod: ${reposClean} | With new errors: ${reposWithErrors}`);
|
||||
console.log(`New typecheck errors: ${totalNewTc} | Codemod warnings: ${totalWarnings}`);
|
||||
console.log('');
|
||||
console.log(`Results: ${OUTPUT_DIR}/summary.json`);
|
||||
}
|
||||
|
||||
main();
|
||||
158
packages/codemod/src/cli.ts
Normal file
158
packages/codemod/src/cli.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/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 && !result.packageJsonChanges) {
|
||||
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 && d.category !== 'v2-gap');
|
||||
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('');
|
||||
}
|
||||
|
||||
const v2Gaps = result.diagnostics.filter(d => d.category === 'v2-gap');
|
||||
if (v2Gaps.length > 0) {
|
||||
console.log(`SDK v2 known issues (${v2Gaps.length}):`);
|
||||
for (const d of v2Gaps) {
|
||||
console.log(formatDiagnostic(d));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (result.packageJsonChanges) {
|
||||
const pc = result.packageJsonChanges;
|
||||
if (opts['dryRun']) {
|
||||
console.log('package.json changes (dry run — not applied):');
|
||||
} else {
|
||||
console.log('package.json updated:');
|
||||
}
|
||||
if (pc.removed.length > 0) {
|
||||
console.log(` Removed: ${pc.removed.join(', ')}`);
|
||||
}
|
||||
if (pc.added.length > 0) {
|
||||
console.log(` Added: ${pc.added.join(', ')}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (opts['dryRun']) {
|
||||
console.log('Run without --dry-run to apply changes.\n');
|
||||
} else {
|
||||
if (result.packageJsonChanges) {
|
||||
console.log('Run your package manager to install the new packages.\n');
|
||||
}
|
||||
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();
|
||||
170
packages/codemod/src/generated/specSchemaMap.ts
Normal file
170
packages/codemod/src/generated/specSchemaMap.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// AUTO-GENERATED — do not edit. Run `pnpm run generate:spec-schemas` to regenerate.
|
||||
export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
||||
'AnnotationsSchema',
|
||||
'AudioContentSchema',
|
||||
'BaseMetadataSchema',
|
||||
'BlobResourceContentsSchema',
|
||||
'BooleanSchemaSchema',
|
||||
'CallToolRequestParamsSchema',
|
||||
'CallToolRequestSchema',
|
||||
'CallToolResultSchema',
|
||||
'CancelTaskRequestSchema',
|
||||
'CancelTaskResultSchema',
|
||||
'CancelledNotificationParamsSchema',
|
||||
'CancelledNotificationSchema',
|
||||
'ClientCapabilitiesSchema',
|
||||
'ClientNotificationSchema',
|
||||
'ClientRequestSchema',
|
||||
'ClientResultSchema',
|
||||
'CompatibilityCallToolResultSchema',
|
||||
'CompleteRequestParamsSchema',
|
||||
'CompleteRequestSchema',
|
||||
'CompleteResultSchema',
|
||||
'ContentBlockSchema',
|
||||
'CreateMessageRequestParamsSchema',
|
||||
'CreateMessageRequestSchema',
|
||||
'CreateMessageResultSchema',
|
||||
'CreateMessageResultWithToolsSchema',
|
||||
'CreateTaskResultSchema',
|
||||
'CursorSchema',
|
||||
'ElicitRequestFormParamsSchema',
|
||||
'ElicitRequestParamsSchema',
|
||||
'ElicitRequestSchema',
|
||||
'ElicitRequestURLParamsSchema',
|
||||
'ElicitResultSchema',
|
||||
'ElicitationCompleteNotificationParamsSchema',
|
||||
'ElicitationCompleteNotificationSchema',
|
||||
'EmbeddedResourceSchema',
|
||||
'EmptyResultSchema',
|
||||
'EnumSchemaSchema',
|
||||
'GetPromptRequestParamsSchema',
|
||||
'GetPromptRequestSchema',
|
||||
'GetPromptResultSchema',
|
||||
'GetTaskPayloadRequestSchema',
|
||||
'GetTaskPayloadResultSchema',
|
||||
'GetTaskRequestSchema',
|
||||
'GetTaskResultSchema',
|
||||
'IconSchema',
|
||||
'IconsSchema',
|
||||
'ImageContentSchema',
|
||||
'ImplementationSchema',
|
||||
'InitializeRequestParamsSchema',
|
||||
'InitializeRequestSchema',
|
||||
'InitializeResultSchema',
|
||||
'InitializedNotificationSchema',
|
||||
'JSONArraySchema',
|
||||
'JSONObjectSchema',
|
||||
'JSONRPCErrorResponseSchema',
|
||||
'JSONRPCMessageSchema',
|
||||
'JSONRPCNotificationSchema',
|
||||
'JSONRPCRequestSchema',
|
||||
'JSONRPCResponseSchema',
|
||||
'JSONRPCResultResponseSchema',
|
||||
'JSONValueSchema',
|
||||
'LegacyTitledEnumSchemaSchema',
|
||||
'ListPromptsRequestSchema',
|
||||
'ListPromptsResultSchema',
|
||||
'ListResourceTemplatesRequestSchema',
|
||||
'ListResourceTemplatesResultSchema',
|
||||
'ListResourcesRequestSchema',
|
||||
'ListResourcesResultSchema',
|
||||
'ListRootsRequestSchema',
|
||||
'ListRootsResultSchema',
|
||||
'ListTasksRequestSchema',
|
||||
'ListTasksResultSchema',
|
||||
'ListToolsRequestSchema',
|
||||
'ListToolsResultSchema',
|
||||
'LoggingLevelSchema',
|
||||
'LoggingMessageNotificationParamsSchema',
|
||||
'LoggingMessageNotificationSchema',
|
||||
'ModelHintSchema',
|
||||
'ModelPreferencesSchema',
|
||||
'MultiSelectEnumSchemaSchema',
|
||||
'NotificationSchema',
|
||||
'NumberSchemaSchema',
|
||||
'OAuthClientInformationFullSchema',
|
||||
'OAuthClientInformationSchema',
|
||||
'OAuthClientMetadataSchema',
|
||||
'OAuthClientRegistrationErrorSchema',
|
||||
'OAuthErrorResponseSchema',
|
||||
'OAuthMetadataSchema',
|
||||
'OAuthProtectedResourceMetadataSchema',
|
||||
'OAuthTokenRevocationRequestSchema',
|
||||
'OAuthTokensSchema',
|
||||
'OpenIdProviderDiscoveryMetadataSchema',
|
||||
'OpenIdProviderMetadataSchema',
|
||||
'PaginatedRequestParamsSchema',
|
||||
'PaginatedRequestSchema',
|
||||
'PaginatedResultSchema',
|
||||
'PingRequestSchema',
|
||||
'PrimitiveSchemaDefinitionSchema',
|
||||
'ProgressNotificationParamsSchema',
|
||||
'ProgressNotificationSchema',
|
||||
'ProgressSchema',
|
||||
'ProgressTokenSchema',
|
||||
'PromptArgumentSchema',
|
||||
'PromptListChangedNotificationSchema',
|
||||
'PromptMessageSchema',
|
||||
'PromptReferenceSchema',
|
||||
'PromptSchema',
|
||||
'ReadResourceRequestParamsSchema',
|
||||
'ReadResourceRequestSchema',
|
||||
'ReadResourceResultSchema',
|
||||
'RelatedTaskMetadataSchema',
|
||||
'RequestIdSchema',
|
||||
'RequestMetaSchema',
|
||||
'RequestSchema',
|
||||
'ResourceContentsSchema',
|
||||
'ResourceLinkSchema',
|
||||
'ResourceListChangedNotificationSchema',
|
||||
'ResourceRequestParamsSchema',
|
||||
'ResourceSchema',
|
||||
'ResourceTemplateReferenceSchema',
|
||||
'ResourceTemplateSchema',
|
||||
'ResourceUpdatedNotificationParamsSchema',
|
||||
'ResourceUpdatedNotificationSchema',
|
||||
'ResultSchema',
|
||||
'RoleSchema',
|
||||
'RootSchema',
|
||||
'RootsListChangedNotificationSchema',
|
||||
'SamplingContentSchema',
|
||||
'SamplingMessageContentBlockSchema',
|
||||
'SamplingMessageSchema',
|
||||
'ServerCapabilitiesSchema',
|
||||
'ServerNotificationSchema',
|
||||
'ServerRequestSchema',
|
||||
'ServerResultSchema',
|
||||
'SetLevelRequestParamsSchema',
|
||||
'SetLevelRequestSchema',
|
||||
'SingleSelectEnumSchemaSchema',
|
||||
'StringSchemaSchema',
|
||||
'SubscribeRequestParamsSchema',
|
||||
'SubscribeRequestSchema',
|
||||
'TaskAugmentedRequestParamsSchema',
|
||||
'TaskCreationParamsSchema',
|
||||
'TaskMetadataSchema',
|
||||
'TaskSchema',
|
||||
'TaskStatusNotificationParamsSchema',
|
||||
'TaskStatusNotificationSchema',
|
||||
'TaskStatusSchema',
|
||||
'TextContentSchema',
|
||||
'TextResourceContentsSchema',
|
||||
'TitledMultiSelectEnumSchemaSchema',
|
||||
'TitledSingleSelectEnumSchemaSchema',
|
||||
'ToolAnnotationsSchema',
|
||||
'ToolChoiceSchema',
|
||||
'ToolExecutionSchema',
|
||||
'ToolListChangedNotificationSchema',
|
||||
'ToolResultContentSchema',
|
||||
'ToolSchema',
|
||||
'ToolUseContentSchema',
|
||||
'UnsubscribeRequestParamsSchema',
|
||||
'UnsubscribeRequestSchema',
|
||||
'UntitledMultiSelectEnumSchemaSchema',
|
||||
'UntitledSingleSelectEnumSchemaSchema'
|
||||
]);
|
||||
|
||||
export function specSchemaToTypeName(schemaName: string): string | undefined {
|
||||
if (!SPEC_SCHEMA_NAMES.has(schemaName)) return undefined;
|
||||
return schemaName.slice(0, -'Schema'.length);
|
||||
}
|
||||
7
packages/codemod/src/generated/versions.ts
Normal file
7
packages/codemod/src/generated/versions.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// AUTO-GENERATED — do not edit. Run `pnpm run generate:versions` to regenerate.
|
||||
export const V2_PACKAGE_VERSIONS: Record<string, string> = {
|
||||
'@modelcontextprotocol/client': '^2.0.0-alpha.2',
|
||||
'@modelcontextprotocol/server': '^2.0.0-alpha.2',
|
||||
'@modelcontextprotocol/node': '^2.0.0-alpha.2',
|
||||
'@modelcontextprotocol/express': '^2.0.0-alpha.2'
|
||||
};
|
||||
14
packages/codemod/src/index.ts
Normal file
14
packages/codemod/src/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export { getMigration, listMigrations } from './migrations/index.js';
|
||||
export { run } from './runner.js';
|
||||
export type {
|
||||
Diagnostic,
|
||||
FileResult,
|
||||
Migration,
|
||||
PackageJsonChange,
|
||||
RunnerOptions,
|
||||
RunnerResult,
|
||||
Transform,
|
||||
TransformContext,
|
||||
TransformResult
|
||||
} from './types.js';
|
||||
export { DiagnosticLevel } from './types.js';
|
||||
12
packages/codemod/src/migrations/index.ts
Normal file
12
packages/codemod/src/migrations/index.ts
Normal file
|
|
@ -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
packages/codemod/src/migrations/v1-to-v2/index.ts
Normal file
8
packages/codemod/src/migrations/v1-to-v2/index.ts
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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';
|
||||
168
packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts
Normal file
168
packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
export interface ImportMapping {
|
||||
target: string;
|
||||
status: 'moved' | 'removed' | 'renamed';
|
||||
renamedSymbols?: Record<string, string>;
|
||||
/** Route specific symbols to a different target package than `target`. */
|
||||
symbolTargetOverrides?: Record<string, string>;
|
||||
removalMessage?: string;
|
||||
/** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */
|
||||
isV2Gap?: boolean;
|
||||
}
|
||||
|
||||
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',
|
||||
symbolTargetOverrides: {
|
||||
StdioClientTransport: '@modelcontextprotocol/client/stdio',
|
||||
DEFAULT_INHERITED_ENV_VARS: '@modelcontextprotocol/client/stdio',
|
||||
getDefaultEnvironment: '@modelcontextprotocol/client/stdio',
|
||||
StdioServerParameters: '@modelcontextprotocol/client/stdio'
|
||||
}
|
||||
},
|
||||
'@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/stdio',
|
||||
status: 'moved'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/server/streamableHttp.js': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'renamed',
|
||||
renamedSymbols: {
|
||||
StreamableHTTPServerTransport: 'NodeStreamableHTTPServerTransport'
|
||||
},
|
||||
symbolTargetOverrides: {
|
||||
StreamableHTTPServerTransport: '@modelcontextprotocol/node'
|
||||
}
|
||||
},
|
||||
'@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/zod-compat.js': {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'zod-compat removed in v2. AnySchema and SchemaOutput types have no v2 equivalent — v2 uses StandardSchemaV1 from @standard-schema/spec. Rewrite generic function signatures to use StandardSchemaV1 directly.'
|
||||
},
|
||||
|
||||
'@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 provider removed in v2. For Resource-Server auth (token verification), see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/server/auth/router.js': {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'Server auth router removed in v2. For metadata endpoints, see mcpAuthMetadataRouter from @modelcontextprotocol/express. For full OAuth AS router, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/server/auth/middleware.js': {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'Server auth middleware removed in v2. For bearer token validation, see requireBearerAuth from @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/server/auth/errors.js': {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'Auth error subclasses consolidated in v2. Use OAuthError + OAuthErrorCode from @modelcontextprotocol/server. See also @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
},
|
||||
|
||||
'@modelcontextprotocol/sdk/types.js': {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
renamedSymbols: {
|
||||
ResourceTemplate: 'ResourceTemplateType'
|
||||
}
|
||||
},
|
||||
'@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/server/completable.js': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'moved'
|
||||
},
|
||||
|
||||
'@modelcontextprotocol/sdk/experimental/tasks': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'moved'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/experimental/tasks.js': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'moved'
|
||||
},
|
||||
|
||||
'@modelcontextprotocol/sdk/inMemory.js': {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved'
|
||||
}
|
||||
};
|
||||
|
||||
export function isAuthImport(specifier: string): boolean {
|
||||
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
export const SCHEMA_TO_METHOD: Record<string, string> = {
|
||||
InitializeRequestSchema: 'initialize',
|
||||
CallToolRequestSchema: 'tools/call',
|
||||
ListToolsRequestSchema: 'tools/list',
|
||||
ListPromptsRequestSchema: 'prompts/list',
|
||||
GetPromptRequestSchema: 'prompts/get',
|
||||
ListResourcesRequestSchema: 'resources/list',
|
||||
ReadResourceRequestSchema: 'resources/read',
|
||||
ListResourceTemplatesRequestSchema: 'resources/templates/list',
|
||||
SubscribeRequestSchema: 'resources/subscribe',
|
||||
UnsubscribeRequestSchema: 'resources/unsubscribe',
|
||||
CreateMessageRequestSchema: 'sampling/createMessage',
|
||||
ElicitRequestSchema: 'elicitation/create',
|
||||
SetLevelRequestSchema: 'logging/setLevel',
|
||||
PingRequestSchema: 'ping',
|
||||
CompleteRequestSchema: 'completion/complete',
|
||||
ListRootsRequestSchema: 'roots/list',
|
||||
ListTasksRequestSchema: 'tasks/list',
|
||||
GetTaskRequestSchema: 'tasks/get',
|
||||
GetTaskPayloadRequestSchema: 'tasks/result',
|
||||
CancelTaskRequestSchema: 'tasks/cancel'
|
||||
};
|
||||
|
||||
export const NOTIFICATION_SCHEMA_TO_METHOD: Record<string, string> = {
|
||||
LoggingMessageNotificationSchema: 'notifications/message',
|
||||
ToolListChangedNotificationSchema: 'notifications/tools/list_changed',
|
||||
ResourceListChangedNotificationSchema: 'notifications/resources/list_changed',
|
||||
PromptListChangedNotificationSchema: 'notifications/prompts/list_changed',
|
||||
ResourceUpdatedNotificationSchema: 'notifications/resources/updated',
|
||||
ProgressNotificationSchema: 'notifications/progress',
|
||||
CancelledNotificationSchema: 'notifications/cancelled',
|
||||
InitializedNotificationSchema: 'notifications/initialized',
|
||||
RootsListChangedNotificationSchema: 'notifications/roots/list_changed',
|
||||
TaskStatusNotificationSchema: 'notifications/tasks/status',
|
||||
ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete'
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue