feat: add v2 codemod draft (#1950)
This commit is contained in:
parent
4f226c1e35
commit
48251fe2cb
29 changed files with 9239 additions and 1 deletions
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'
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const SIMPLE_RENAMES: Record<string, string> = {
|
||||
McpError: 'ProtocolError',
|
||||
JSONRPCError: 'JSONRPCErrorResponse',
|
||||
JSONRPCErrorSchema: 'JSONRPCErrorResponseSchema',
|
||||
isJSONRPCError: 'isJSONRPCErrorResponse',
|
||||
isJSONRPCResponse: 'isJSONRPCResultResponse',
|
||||
ResourceReference: 'ResourceTemplateReference',
|
||||
ResourceReferenceSchema: 'ResourceTemplateReferenceSchema'
|
||||
};
|
||||
|
||||
export const ERROR_CODE_SDK_MEMBERS = new Set(['RequestTimeout', 'ConnectionClosed']);
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { info, warning } from '../../../utils/diagnostics.js';
|
||||
import { hasMcpImports } from '../../../utils/importUtils.js';
|
||||
import { CONTEXT_PROPERTY_MAP, CTX_PARAM_NAME, EXTRA_PARAM_NAME } from '../mappings/contextPropertyMap.js';
|
||||
|
||||
const HANDLER_METHODS = new Set(['setRequestHandler', 'setNotificationHandler']);
|
||||
|
||||
const REGISTER_METHODS = new Set(['registerTool', 'registerPrompt', 'registerResource', 'registerToolTask', 'tool', 'prompt', 'resource']);
|
||||
|
||||
/**
|
||||
* Attempt to rename the second parameter of a callback from 'extra' to 'ctx'
|
||||
* and rewrite context property accesses in its body.
|
||||
* Returns the number of changes made, or -1 if skipped.
|
||||
*/
|
||||
function processCallback(
|
||||
callbackNode: Node,
|
||||
sourceFile: SourceFile,
|
||||
diagnostics: Diagnostic[],
|
||||
methodName: string,
|
||||
callLine: number
|
||||
): number {
|
||||
if (!Node.isArrowFunction(callbackNode) && !Node.isFunctionExpression(callbackNode) && !Node.isMethodDeclaration(callbackNode))
|
||||
return -1;
|
||||
|
||||
const params = callbackNode.getParameters();
|
||||
if (params.length < 2) return -1;
|
||||
|
||||
const extraParam = params[1]!;
|
||||
const paramNameNode = extraParam.getNameNode();
|
||||
if (Node.isObjectBindingPattern(paramNameNode)) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
extraParam.getStartLineNumber(),
|
||||
`Destructuring of context parameter in signature: "${paramNameNode.getText()}". ` +
|
||||
'Properties have been reorganized in v2 (e.g., signal is now ctx.mcpReq.signal). Manual refactoring required.'
|
||||
)
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
const paramName = extraParam.getName();
|
||||
if (paramName !== EXTRA_PARAM_NAME) return -1;
|
||||
|
||||
const body = callbackNode.getBody();
|
||||
|
||||
const otherParams = callbackNode.getParameters().filter(p => p !== extraParam);
|
||||
if (otherParams.some(p => p.getName() === CTX_PARAM_NAME)) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
extraParam.getStartLineNumber(),
|
||||
`Cannot rename '${EXTRA_PARAM_NAME}' to '${CTX_PARAM_NAME}': another parameter is already named '${CTX_PARAM_NAME}'. Manual migration required.`
|
||||
)
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (body) {
|
||||
let ctxAlreadyInScope = false;
|
||||
body.forEachDescendant((node, traversal) => {
|
||||
if (
|
||||
(Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isFunctionDeclaration(node)) &&
|
||||
node.getParameters().some(p => p.getName() === CTX_PARAM_NAME)
|
||||
) {
|
||||
traversal.skip();
|
||||
return;
|
||||
}
|
||||
if (Node.isIdentifier(node) && node.getText() === CTX_PARAM_NAME) {
|
||||
ctxAlreadyInScope = true;
|
||||
}
|
||||
});
|
||||
if (ctxAlreadyInScope) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
extraParam.getStartLineNumber(),
|
||||
`Cannot rename '${EXTRA_PARAM_NAME}' to '${CTX_PARAM_NAME}': '${CTX_PARAM_NAME}' is already referenced in this scope. Manual migration required.`
|
||||
)
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Rename param declaration and rewrite body references using AST traversal.
|
||||
// We walk Identifier nodes to avoid corrupting string literals, comments, and
|
||||
// unrelated property names (e.g., meta.extra) that regex-based replacement would hit.
|
||||
const paramDecl = extraParam.getNameNode();
|
||||
paramDecl.replaceWithText(CTX_PARAM_NAME);
|
||||
|
||||
if (body) {
|
||||
const sortedMappings = [...CONTEXT_PROPERTY_MAP].filter(m => m.from !== m.to).toSorted((a, b) => b.from.length - a.from.length);
|
||||
|
||||
// Collect identifiers that are actual references to the `extra` parameter
|
||||
const identifiers: import('ts-morph').Node[] = [];
|
||||
body.forEachDescendant(node => {
|
||||
if (!Node.isIdentifier(node) || node.getText() !== EXTRA_PARAM_NAME) return;
|
||||
const parent = node.getParent();
|
||||
// Skip property-name positions (e.g., meta.extra, { extra: value }, { extra }, { extra: x } = obj)
|
||||
if (parent && Node.isPropertyAccessExpression(parent) && parent.getNameNode() === node) return;
|
||||
if (parent && Node.isPropertyAssignment(parent) && parent.getNameNode() === node) return;
|
||||
if (parent && Node.isShorthandPropertyAssignment(parent)) return;
|
||||
if (parent && Node.isBindingElement(parent) && parent.getPropertyNameNode() === node) return;
|
||||
identifiers.push(node);
|
||||
});
|
||||
|
||||
// Build replacements: apply property mappings for PropertyAccess/QualifiedName, plain rename otherwise
|
||||
const replacements: { node: import('ts-morph').Node; newText: string }[] = [];
|
||||
for (const id of identifiers) {
|
||||
const parent = id.getParent();
|
||||
// Value-position property access: extra.signal → ctx.mcpReq.signal
|
||||
if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === id) {
|
||||
const propName = '.' + parent.getName();
|
||||
const mapping = sortedMappings.find(m => m.from === propName);
|
||||
if (mapping) {
|
||||
replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Type-position qualified name: typeof extra.signal → typeof ctx.mcpReq.signal
|
||||
if (parent && parent.getKind() === SyntaxKind.QualifiedName && parent.getChildAtIndex(0) === id) {
|
||||
const right = parent.getChildAtIndex(2);
|
||||
if (right) {
|
||||
const propName = '.' + right.getText();
|
||||
const mapping = sortedMappings.find(m => m.from === propName);
|
||||
if (mapping) {
|
||||
replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
replacements.push({ node: id, newText: CTX_PARAM_NAME });
|
||||
}
|
||||
|
||||
// Apply in reverse position order to avoid node invalidation
|
||||
const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart());
|
||||
for (const { node, newText } of sorted) {
|
||||
node.replaceWithText(newText);
|
||||
}
|
||||
}
|
||||
|
||||
const changes = 1;
|
||||
|
||||
if (['tool', 'prompt', 'resource'].includes(methodName)) {
|
||||
diagnostics.push(
|
||||
info(
|
||||
sourceFile.getFilePath(),
|
||||
callLine,
|
||||
`Renamed 'extra' to 'ctx' in .${methodName}() callback. If this is not an McpServer method, revert this change.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Warn on destructuring of ctx in body (after text replacement)
|
||||
const freshBody = callbackNode.getBody();
|
||||
if (freshBody) {
|
||||
freshBody.forEachDescendant(node => {
|
||||
if (!Node.isVariableDeclaration(node)) return;
|
||||
const initializer = node.getInitializer();
|
||||
if (!initializer || !Node.isIdentifier(initializer) || initializer.getText() !== CTX_PARAM_NAME) return;
|
||||
const nameNode = node.getNameNode();
|
||||
if (!Node.isObjectBindingPattern(nameNode)) return;
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
`Destructuring of context parameter detected: "const ${nameNode.getText()} = ${CTX_PARAM_NAME}". ` +
|
||||
'Properties have been reorganized in v2 (e.g., signal is now ctx.mcpReq.signal). Manual refactoring required.'
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export const contextTypesTransform: Transform = {
|
||||
name: 'Context type rewrites',
|
||||
id: 'context',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
if (!hasMcpImports(sourceFile)) {
|
||||
return { changesCount: 0, diagnostics: [] };
|
||||
}
|
||||
|
||||
let changesCount = 0;
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
// Process one callback at a time, re-querying the AST after each.
|
||||
// processCallback uses body.replaceWithText() which invalidates sibling nodes,
|
||||
// so we cannot iterate a pre-collected list of calls.
|
||||
let madeProgress = true;
|
||||
const processed = new Set<number>();
|
||||
while (madeProgress) {
|
||||
madeProgress = false;
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
for (const call of calls) {
|
||||
const callStart = call.getStart();
|
||||
if (processed.has(callStart)) continue;
|
||||
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) continue;
|
||||
|
||||
const methodName = expr.getName();
|
||||
const isHandler = HANDLER_METHODS.has(methodName);
|
||||
const isRegister = REGISTER_METHODS.has(methodName);
|
||||
if (!isHandler && !isRegister) continue;
|
||||
|
||||
const args = call.getArguments();
|
||||
|
||||
let callbackArg: Node | undefined;
|
||||
if (isHandler && args.length >= 2) {
|
||||
callbackArg = args[1];
|
||||
} else if (isRegister && args.length >= 2) {
|
||||
callbackArg = args.at(-1);
|
||||
}
|
||||
|
||||
if (!callbackArg) continue;
|
||||
|
||||
// Handle ObjectLiteralExpression for registerToolTask-style callbacks
|
||||
if (Node.isObjectLiteralExpression(callbackArg)) {
|
||||
for (const prop of callbackArg.getProperties()) {
|
||||
let callbackNode: Node | undefined;
|
||||
if (Node.isPropertyAssignment(prop)) {
|
||||
callbackNode = prop.getInitializer();
|
||||
} else if (Node.isMethodDeclaration(prop)) {
|
||||
callbackNode = prop;
|
||||
}
|
||||
if (!callbackNode) continue;
|
||||
|
||||
const result = processCallback(callbackNode, sourceFile, diagnostics, methodName, call.getStartLineNumber());
|
||||
if (result > 0) {
|
||||
changesCount += result;
|
||||
madeProgress = true;
|
||||
}
|
||||
}
|
||||
processed.add(callStart);
|
||||
if (madeProgress) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle direct ArrowFunction / FunctionExpression callbacks
|
||||
const result = processCallback(callbackArg, sourceFile, diagnostics, methodName, call.getStartLineNumber());
|
||||
processed.add(callStart);
|
||||
if (result > 0) {
|
||||
changesCount += result;
|
||||
madeProgress = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { info } from '../../../utils/diagnostics.js';
|
||||
import { isOriginalNameImportedFromMcp, resolveLocalImportName } from '../../../utils/importUtils.js';
|
||||
|
||||
export const expressMiddlewareTransform: Transform = {
|
||||
name: 'Express middleware signature migration',
|
||||
id: 'express-middleware',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
if (!isOriginalNameImportedFromMcp(sourceFile, 'hostHeaderValidation')) {
|
||||
return { changesCount: 0, diagnostics: [] };
|
||||
}
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
let changesCount = 0;
|
||||
|
||||
const localName = resolveLocalImportName(sourceFile, 'hostHeaderValidation') ?? 'hostHeaderValidation';
|
||||
changesCount += rewriteHostHeaderValidation(sourceFile, localName, diagnostics);
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
||||
function rewriteHostHeaderValidation(sourceFile: SourceFile, targetName: string, diagnostics: Diagnostic[]): number {
|
||||
let changesCount = 0;
|
||||
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
for (const call of calls) {
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isIdentifier(expr) || expr.getText() !== targetName) continue;
|
||||
|
||||
const args = call.getArguments();
|
||||
if (args.length !== 1) continue;
|
||||
|
||||
const firstArg = args[0]!;
|
||||
if (!Node.isObjectLiteralExpression(firstArg)) continue;
|
||||
|
||||
const allowedHostsProp = firstArg.getProperty('allowedHosts');
|
||||
if (!allowedHostsProp || !Node.isPropertyAssignment(allowedHostsProp)) continue;
|
||||
|
||||
const initializer = allowedHostsProp.getInitializer();
|
||||
if (!initializer) continue;
|
||||
|
||||
const arrayText = initializer.getText();
|
||||
firstArg.replaceWithText(arrayText);
|
||||
changesCount++;
|
||||
|
||||
diagnostics.push(
|
||||
info(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
'hostHeaderValidation({ allowedHosts: [...] }) simplified to hostHeaderValidation([...]). Verify the migration.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return changesCount;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { warning } from '../../../utils/diagnostics.js';
|
||||
import { isImportedFromMcp, removeUnusedImport, resolveOriginalImportName } from '../../../utils/importUtils.js';
|
||||
import { NOTIFICATION_SCHEMA_TO_METHOD, SCHEMA_TO_METHOD } from '../mappings/schemaToMethodMap.js';
|
||||
|
||||
const ALL_SCHEMA_TO_METHOD: Record<string, string> = {
|
||||
...SCHEMA_TO_METHOD,
|
||||
...NOTIFICATION_SCHEMA_TO_METHOD
|
||||
};
|
||||
|
||||
export const handlerRegistrationTransform: Transform = {
|
||||
name: 'Handler registration migration',
|
||||
id: 'handlers',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
let changesCount = 0;
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
for (const call of calls) {
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) continue;
|
||||
|
||||
const methodName = expr.getName();
|
||||
if (methodName !== 'setRequestHandler' && methodName !== 'setNotificationHandler') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const args = call.getArguments();
|
||||
if (args.length < 2) continue;
|
||||
|
||||
const firstArg = args[0]!;
|
||||
if (!Node.isIdentifier(firstArg)) continue;
|
||||
|
||||
const schemaName = firstArg.getText();
|
||||
const originalName = resolveOriginalImportName(sourceFile, schemaName) ?? schemaName;
|
||||
const methodString = ALL_SCHEMA_TO_METHOD[originalName];
|
||||
if (!methodString) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
`Custom method handler: ${methodName}(${schemaName}, ...). ` +
|
||||
`In v2, use the 3-arg form: ${methodName}('method/name', { params, result? }, handler). ` +
|
||||
`See migration.md for details.`
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isImportedFromMcp(sourceFile, schemaName)) continue;
|
||||
|
||||
firstArg.replaceWithText(`'${methodString}'`);
|
||||
changesCount++;
|
||||
|
||||
removeUnusedImport(sourceFile, schemaName, true);
|
||||
}
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { renameAllReferences } from '../../../utils/astUtils.js';
|
||||
import { v2Gap, warning } from '../../../utils/diagnostics.js';
|
||||
import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js';
|
||||
import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js';
|
||||
import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js';
|
||||
import { SIMPLE_RENAMES } from '../mappings/symbolMap.js';
|
||||
|
||||
const REEXPORT_WARNINGS: Record<string, string> = {
|
||||
ErrorCode: 'Re-exported ErrorCode was split into ProtocolErrorCode and SdkErrorCode in v2. Update this re-export manually.',
|
||||
RequestHandlerExtra:
|
||||
'Re-exported RequestHandlerExtra was renamed to ServerContext/ClientContext in v2. Update this re-export manually.',
|
||||
IsomorphicHeaders: 'Re-exported IsomorphicHeaders was removed in v2 (replaced by standard Headers API). Remove this re-export.',
|
||||
StreamableHTTPError:
|
||||
'Re-exported StreamableHTTPError was renamed to SdkError in v2 with different constructor. Update this re-export manually.'
|
||||
};
|
||||
|
||||
export const importPathsTransform: Transform = {
|
||||
name: 'Import path rewrites',
|
||||
id: 'imports',
|
||||
apply(sourceFile: SourceFile, context: TransformContext): TransformResult {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const usedPackages = new Set<string>();
|
||||
let changesCount = 0;
|
||||
|
||||
const sdkImports = getSdkImports(sourceFile);
|
||||
const sdkExports = getSdkExports(sourceFile);
|
||||
if (sdkImports.length === 0 && sdkExports.length === 0) {
|
||||
return { changesCount: 0, diagnostics: [] };
|
||||
}
|
||||
|
||||
const filePath = sourceFile.getFilePath();
|
||||
|
||||
changesCount += rewriteExportDeclarations(sdkExports, sourceFile, filePath, context, diagnostics, usedPackages);
|
||||
|
||||
if (sdkImports.length === 0) {
|
||||
return { changesCount, diagnostics, usedPackages };
|
||||
}
|
||||
|
||||
const hasClientImport = sdkImports.some(imp => {
|
||||
const spec = imp.getModuleSpecifierValue();
|
||||
return spec.includes('/client/');
|
||||
});
|
||||
const hasServerImport = sdkImports.some(imp => {
|
||||
const spec = imp.getModuleSpecifierValue();
|
||||
return spec.includes('/server/');
|
||||
});
|
||||
|
||||
const insertIndex = sourceFile.getImportDeclarations().indexOf(sdkImports[0]!);
|
||||
|
||||
interface PendingImport {
|
||||
names: string[];
|
||||
isTypeOnly: boolean;
|
||||
}
|
||||
const pendingImports = new Map<string, PendingImport[]>();
|
||||
|
||||
function addPending(target: string, names: string[], isTypeOnly: boolean): void {
|
||||
if (!pendingImports.has(target)) {
|
||||
pendingImports.set(target, []);
|
||||
}
|
||||
pendingImports.get(target)!.push({ names, isTypeOnly });
|
||||
}
|
||||
|
||||
for (const imp of sdkImports) {
|
||||
const specifier = imp.getModuleSpecifierValue();
|
||||
const namedImports = imp.getNamedImports();
|
||||
const typeOnly = isTypeOnlyImport(imp);
|
||||
const line = imp.getStartLineNumber();
|
||||
const defaultImport = imp.getDefaultImport();
|
||||
const namespaceImport = imp.getNamespaceImport();
|
||||
|
||||
let mapping = IMPORT_MAP[specifier];
|
||||
|
||||
if (!mapping && isAuthImport(specifier)) {
|
||||
mapping = {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'Server auth removed in v2. For RS auth, see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
};
|
||||
}
|
||||
|
||||
if (!mapping) {
|
||||
diagnostics.push(warning(filePath, line, `Unknown SDK import path: ${specifier}. Manual migration required.`));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapping.status === 'removed') {
|
||||
imp.remove();
|
||||
changesCount++;
|
||||
const diagFn = mapping.isV2Gap ? v2Gap : warning;
|
||||
diagnostics.push(diagFn(filePath, line, mapping.removalMessage ?? `Import removed: ${specifier}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
let targetPackage = mapping.target;
|
||||
if (targetPackage === 'RESOLVE_BY_CONTEXT') {
|
||||
targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport, {
|
||||
filePath,
|
||||
line,
|
||||
diagnostics
|
||||
});
|
||||
}
|
||||
|
||||
const symbolsToRenameInFile: Array<[string, string]> = [];
|
||||
if (mapping.renamedSymbols) {
|
||||
for (const [oldName, newName] of Object.entries(mapping.renamedSymbols)) {
|
||||
const matchingImport = namedImports.find(n => n.getName() === oldName);
|
||||
if (matchingImport && !matchingImport.getAliasNode()) {
|
||||
symbolsToRenameInFile.push([oldName, newName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasAlias = namedImports.some(n => n.getAliasNode() !== undefined);
|
||||
if (defaultImport || namespaceImport || hasAlias) {
|
||||
let effectiveTarget = targetPackage;
|
||||
if (mapping.symbolTargetOverrides && !namespaceImport && !defaultImport) {
|
||||
const allOverridden = namedImports.length > 0 && namedImports.every(n => n.getName() in mapping.symbolTargetOverrides!);
|
||||
if (allOverridden) {
|
||||
effectiveTarget = mapping.symbolTargetOverrides[namedImports[0]!.getName()]!;
|
||||
} else if (namedImports.some(n => n.getName() in mapping.symbolTargetOverrides!)) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
filePath,
|
||||
line,
|
||||
`Aliased import from ${specifier} mixes symbols that belong to different v2 packages. ` +
|
||||
`Split the import manually so each symbol targets the correct package.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
usedPackages.add(effectiveTarget);
|
||||
imp.setModuleSpecifier(effectiveTarget);
|
||||
if (mapping.renamedSymbols) {
|
||||
for (const n of namedImports) {
|
||||
const newName = mapping.renamedSymbols[n.getName()];
|
||||
if (newName) {
|
||||
n.setName(newName);
|
||||
}
|
||||
}
|
||||
if (namespaceImport) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
filePath,
|
||||
line,
|
||||
`Namespace import of ${specifier}: exported symbol(s) ${Object.keys(mapping.renamedSymbols).join(', ')} ` +
|
||||
`were renamed in ${effectiveTarget}. Update qualified accesses manually.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
changesCount++;
|
||||
for (const [oldName, newName] of symbolsToRenameInFile) {
|
||||
renameAllReferences(sourceFile, oldName, newName);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const n of namedImports) {
|
||||
const name = n.getName();
|
||||
const resolvedName = mapping.renamedSymbols?.[name] ?? name;
|
||||
const specifierTypeOnly = typeOnly || n.isTypeOnly();
|
||||
const symbolTarget = mapping.symbolTargetOverrides?.[name] ?? targetPackage;
|
||||
usedPackages.add(symbolTarget);
|
||||
addPending(symbolTarget, [resolvedName], specifierTypeOnly);
|
||||
}
|
||||
imp.remove();
|
||||
changesCount++;
|
||||
for (const [oldName, newName] of symbolsToRenameInFile) {
|
||||
renameAllReferences(sourceFile, oldName, newName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [target, groups] of pendingImports) {
|
||||
const typeOnlyNames = new Set<string>();
|
||||
const valueNames = new Set<string>();
|
||||
for (const group of groups) {
|
||||
for (const name of group.names) {
|
||||
if (group.isTypeOnly) {
|
||||
typeOnlyNames.add(name);
|
||||
} else {
|
||||
valueNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valueNames.size > 0) {
|
||||
addOrMergeImport(sourceFile, target, [...valueNames], false, insertIndex);
|
||||
}
|
||||
if (typeOnlyNames.size > 0) {
|
||||
const typeInsertIndex = valueNames.size > 0 ? insertIndex + 1 : insertIndex;
|
||||
addOrMergeImport(sourceFile, target, [...typeOnlyNames], true, typeInsertIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return { changesCount, diagnostics, usedPackages };
|
||||
}
|
||||
};
|
||||
|
||||
function rewriteExportDeclarations(
|
||||
sdkExports: import('ts-morph').ExportDeclaration[],
|
||||
sourceFile: import('ts-morph').SourceFile,
|
||||
filePath: string,
|
||||
context: TransformContext,
|
||||
diagnostics: Diagnostic[],
|
||||
usedPackages: Set<string>
|
||||
): number {
|
||||
let changesCount = 0;
|
||||
|
||||
for (const exp of sdkExports) {
|
||||
const specifier = exp.getModuleSpecifierValue();
|
||||
if (!specifier) continue;
|
||||
|
||||
const line = exp.getStartLineNumber();
|
||||
let mapping = IMPORT_MAP[specifier];
|
||||
|
||||
if (!mapping && isAuthImport(specifier)) {
|
||||
mapping = {
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage:
|
||||
'Server auth removed in v2. For RS auth, see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
|
||||
};
|
||||
}
|
||||
|
||||
if (!mapping) {
|
||||
diagnostics.push(warning(filePath, line, `Unknown SDK export path: ${specifier}. Manual migration required.`));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapping.status === 'removed') {
|
||||
exp.remove();
|
||||
changesCount++;
|
||||
const diagFn = mapping.isV2Gap ? v2Gap : warning;
|
||||
diagnostics.push(diagFn(filePath, line, mapping.removalMessage ?? `Export removed: ${specifier}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
let targetPackage = mapping.target;
|
||||
if (targetPackage === 'RESOLVE_BY_CONTEXT') {
|
||||
const hasClientImport = sourceFile.getImportDeclarations().some(imp => {
|
||||
const spec = imp.getModuleSpecifierValue();
|
||||
return spec.includes('/client/') || spec === '@modelcontextprotocol/client';
|
||||
});
|
||||
const hasServerImport = sourceFile.getImportDeclarations().some(imp => {
|
||||
const spec = imp.getModuleSpecifierValue();
|
||||
return spec.includes('/server/') || spec === '@modelcontextprotocol/server';
|
||||
});
|
||||
targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport);
|
||||
}
|
||||
|
||||
if (mapping.symbolTargetOverrides) {
|
||||
const namedExports = exp.getNamedExports();
|
||||
const allOverridden = namedExports.length > 0 && namedExports.every(s => s.getName() in mapping.symbolTargetOverrides!);
|
||||
if (allOverridden) {
|
||||
targetPackage = mapping.symbolTargetOverrides[namedExports[0]!.getName()]!;
|
||||
} else if (namedExports.some(s => s.getName() in mapping.symbolTargetOverrides!)) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
filePath,
|
||||
line,
|
||||
`Re-export from ${specifier} mixes symbols that belong to different v2 packages. ` +
|
||||
`Split the export manually so each symbol targets the correct package.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
usedPackages.add(targetPackage);
|
||||
exp.setModuleSpecifier(targetPackage);
|
||||
for (const spec of exp.getNamedExports()) {
|
||||
const name = spec.getName();
|
||||
const newName = mapping.renamedSymbols?.[name] ?? SIMPLE_RENAMES[name];
|
||||
if (newName) {
|
||||
if (!spec.getAliasNode()) spec.setAlias(name);
|
||||
spec.setName(newName);
|
||||
}
|
||||
if (REEXPORT_WARNINGS[name]) {
|
||||
diagnostics.push(warning(filePath, line, REEXPORT_WARNINGS[name]!));
|
||||
}
|
||||
}
|
||||
changesCount++;
|
||||
}
|
||||
|
||||
return changesCount;
|
||||
}
|
||||
51
packages/codemod/src/migrations/v1-to-v2/transforms/index.ts
Normal file
51
packages/codemod/src/migrations/v1-to-v2/transforms/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { Transform } from '../../../types.js';
|
||||
import { contextTypesTransform } from './contextTypes.js';
|
||||
import { expressMiddlewareTransform } from './expressMiddleware.js';
|
||||
import { handlerRegistrationTransform } from './handlerRegistration.js';
|
||||
import { importPathsTransform } from './importPaths.js';
|
||||
import { mcpServerApiTransform } from './mcpServerApi.js';
|
||||
import { mockPathsTransform } from './mockPaths.js';
|
||||
import { removedApisTransform } from './removedApis.js';
|
||||
import { schemaParamRemovalTransform } from './schemaParamRemoval.js';
|
||||
import { specSchemaAccessTransform } from './specSchemaAccess.js';
|
||||
import { symbolRenamesTransform } from './symbolRenames.js';
|
||||
|
||||
// Ordering matters — do not reorder without understanding dependencies:
|
||||
//
|
||||
// 1. importPaths MUST run first: rewrites import specifiers from v1 paths
|
||||
// (e.g., @modelcontextprotocol/sdk/types.js) to v2 packages. Later
|
||||
// transforms depend on the rewritten import declarations.
|
||||
//
|
||||
// 2. symbolRenames runs early: renames imported symbols (e.g., McpError →
|
||||
// ProtocolError) and rewrites type references (e.g., SchemaInput<T> →
|
||||
// StandardSchemaWithJSON.InferInput<T>).
|
||||
//
|
||||
// 3. removedApis runs after symbolRenames: handles removed Zod helpers,
|
||||
// IsomorphicHeaders, and StreamableHTTPError. Conceptually different
|
||||
// from renames — these are removals with diagnostic guidance.
|
||||
//
|
||||
// 4. mcpServerApi SHOULD run before contextTypes: it rewrites .tool() etc.
|
||||
// to .registerTool() etc. contextTypes handles both old and new names,
|
||||
// but running mcpServerApi first ensures consistent argument structure.
|
||||
//
|
||||
// 5. handlerRegistration, schemaParamRemoval, and expressMiddleware are
|
||||
// independent of each other but all depend on importPaths having run.
|
||||
//
|
||||
// 6. specSchemaAccess runs after handlerRegistration and schemaParamRemoval:
|
||||
// those transforms remove spec schema references they handle. specSchemaAccess
|
||||
// then processes remaining standalone usages (safeParse, parse, z.infer, etc.).
|
||||
//
|
||||
// 7. mockPaths runs last: handles test mocks and dynamic imports,
|
||||
// independent of the other transforms.
|
||||
export const v1ToV2Transforms: Transform[] = [
|
||||
importPathsTransform,
|
||||
symbolRenamesTransform,
|
||||
removedApisTransform,
|
||||
mcpServerApiTransform,
|
||||
handlerRegistrationTransform,
|
||||
schemaParamRemovalTransform,
|
||||
specSchemaAccessTransform,
|
||||
expressMiddlewareTransform,
|
||||
contextTypesTransform,
|
||||
mockPathsTransform
|
||||
];
|
||||
|
|
@ -0,0 +1,523 @@
|
|||
import type { CallExpression, SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { info, warning } from '../../../utils/diagnostics.js';
|
||||
import { isOriginalNameImportedFromMcp, resolveLocalImportName } from '../../../utils/importUtils.js';
|
||||
|
||||
export const mcpServerApiTransform: Transform = {
|
||||
name: 'McpServer API migration',
|
||||
id: 'mcpserver-api',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
let changesCount = 0;
|
||||
|
||||
if (!isOriginalNameImportedFromMcp(sourceFile, 'McpServer')) {
|
||||
return { changesCount: 0, diagnostics: [] };
|
||||
}
|
||||
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
const toolCalls: CallExpression[] = [];
|
||||
const promptCalls: CallExpression[] = [];
|
||||
const resourceCalls: CallExpression[] = [];
|
||||
const registerToolCalls: CallExpression[] = [];
|
||||
const registerPromptCalls: CallExpression[] = [];
|
||||
const registerResourceCalls: CallExpression[] = [];
|
||||
|
||||
for (const call of calls) {
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) continue;
|
||||
const methodName = expr.getName();
|
||||
|
||||
switch (methodName) {
|
||||
case 'tool': {
|
||||
toolCalls.push(call);
|
||||
break;
|
||||
}
|
||||
case 'prompt': {
|
||||
promptCalls.push(call);
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
resourceCalls.push(call);
|
||||
break;
|
||||
}
|
||||
case 'registerTool': {
|
||||
registerToolCalls.push(call);
|
||||
break;
|
||||
}
|
||||
case 'registerPrompt': {
|
||||
registerPromptCalls.push(call);
|
||||
break;
|
||||
}
|
||||
case 'registerResource': {
|
||||
registerResourceCalls.push(call);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of toolCalls) {
|
||||
const result = migrateToolCall(call, sourceFile, diagnostics);
|
||||
if (result) {
|
||||
changesCount++;
|
||||
} else {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
'Could not automatically migrate .tool() call. Manual migration required.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of promptCalls) {
|
||||
const result = migratePromptCall(call, sourceFile, diagnostics);
|
||||
if (result) {
|
||||
changesCount++;
|
||||
} else {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
'Could not automatically migrate .prompt() call. Manual migration required.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of resourceCalls) {
|
||||
const result = migrateResourceCall(call, sourceFile);
|
||||
if (result) {
|
||||
changesCount++;
|
||||
} else {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
'Could not automatically migrate .resource() call. Manual migration required.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of registerToolCalls) {
|
||||
if (wrapSchemaInConfig(call, 'inputSchema', sourceFile, diagnostics)) {
|
||||
changesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of registerPromptCalls) {
|
||||
if (wrapSchemaInConfig(call, 'argsSchema', sourceFile, diagnostics)) {
|
||||
changesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of registerResourceCalls) {
|
||||
if (wrapSchemaInConfig(call, 'uriSchema', sourceFile, diagnostics)) {
|
||||
changesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
||||
function isStringArg(node: Node): boolean {
|
||||
return Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node) || Node.isTemplateExpression(node);
|
||||
}
|
||||
|
||||
function wrapWithZObject(schemaText: string): string {
|
||||
return `z.object(${schemaText})`;
|
||||
}
|
||||
|
||||
function maybeWrapSchema(node: Node): string {
|
||||
const text = node.getText();
|
||||
if (Node.isObjectLiteralExpression(node)) {
|
||||
return wrapWithZObject(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function emitWrapDiagnostic(node: Node, sourceFile: SourceFile, call: CallExpression, diagnostics: Diagnostic[]): void {
|
||||
if (Node.isObjectLiteralExpression(node)) {
|
||||
diagnostics.push(
|
||||
info(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
'Raw object literal wrapped with z.object(). Verify that zod (z) is imported in this file.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For existing registerTool/registerPrompt/registerResource calls,
|
||||
* wrap the specified schema property with z.object() if it's a raw object literal.
|
||||
*/
|
||||
function wrapSchemaInConfig(call: CallExpression, schemaPropertyName: string, sourceFile: SourceFile, diagnostics: Diagnostic[]): boolean {
|
||||
const args = call.getArguments();
|
||||
// registerTool/registerPrompt: (name, config, callback)
|
||||
// registerResource: (name, uri, config, callback)
|
||||
// Find the config argument by looking for an object literal
|
||||
let configArg: Node | undefined;
|
||||
for (const arg of args) {
|
||||
if (Node.isObjectLiteralExpression(arg)) {
|
||||
configArg = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configArg || !Node.isObjectLiteralExpression(configArg)) return false;
|
||||
|
||||
const schemaProp = configArg.getProperty(schemaPropertyName);
|
||||
if (!schemaProp) return false;
|
||||
|
||||
if (Node.isShorthandPropertyAssignment(schemaProp)) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
`Shorthand \`{ ${schemaPropertyName} }\` in config: verify the value is a z.object() schema, not a raw object. V2 requires a Zod schema.`
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Node.isPropertyAssignment(schemaProp)) return false;
|
||||
|
||||
const initializer = schemaProp.getInitializer();
|
||||
if (!initializer) return false;
|
||||
|
||||
if (Node.isObjectLiteralExpression(initializer)) {
|
||||
const wrapped = wrapWithZObject(initializer.getText());
|
||||
initializer.replaceWithText(wrapped);
|
||||
diagnostics.push(
|
||||
info(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
`Raw object literal in ${schemaPropertyName} wrapped with z.object(). Verify that zod (z) is imported in this file.`
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
`\`${schemaPropertyName}\` value is not an object literal — verify it is a z.object() schema. V2 requires a Zod schema, not a raw object.`
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
function migrateToolCall(call: CallExpression, sourceFile: SourceFile, diagnostics: Diagnostic[]): boolean {
|
||||
const args = call.getArguments();
|
||||
if (args.length < 2) return false;
|
||||
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) return false;
|
||||
|
||||
const nameArg = args[0]!;
|
||||
if (!isStringArg(nameArg)) return false;
|
||||
const nameText = nameArg.getText();
|
||||
|
||||
let description: string | undefined;
|
||||
let schema: string | undefined;
|
||||
let annotations: string | undefined;
|
||||
let callbackText: string | undefined;
|
||||
|
||||
switch (args.length) {
|
||||
case 2: {
|
||||
// server.tool(name, callback)
|
||||
callbackText = args[1]!.getText();
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
const arg1 = args[1]!;
|
||||
if (isStringArg(arg1)) {
|
||||
// server.tool(name, description, callback)
|
||||
description = arg1.getText();
|
||||
callbackText = args[2]!.getText();
|
||||
} else {
|
||||
// server.tool(name, schema, callback)
|
||||
emitWrapDiagnostic(arg1, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(arg1);
|
||||
callbackText = args[2]!.getText();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
const arg1 = args[1]!;
|
||||
if (isStringArg(arg1)) {
|
||||
// server.tool(name, description, schema, callback)
|
||||
description = arg1.getText();
|
||||
emitWrapDiagnostic(args[2]!, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(args[2]!);
|
||||
} else {
|
||||
// server.tool(name, schema, annotations, callback)
|
||||
emitWrapDiagnostic(arg1, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(arg1);
|
||||
annotations = args[2]!.getText();
|
||||
}
|
||||
callbackText = args[3]!.getText();
|
||||
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
// server.tool(name, description, schema, annotations, callback)
|
||||
description = args[1]!.getText();
|
||||
emitWrapDiagnostic(args[2]!, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(args[2]!);
|
||||
annotations = args[3]!.getText();
|
||||
callbackText = args[4]!.getText();
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const configParts: string[] = [];
|
||||
if (description) configParts.push(`description: ${description}`);
|
||||
if (schema) configParts.push(`inputSchema: ${schema}`);
|
||||
if (annotations) configParts.push(`annotations: ${annotations}`);
|
||||
const configObj = configParts.length > 0 ? `{ ${configParts.join(', ')} }` : '{}';
|
||||
|
||||
expr.getNameNode().replaceWithText('registerTool');
|
||||
for (let i = args.length - 1; i >= 0; i--) {
|
||||
call.removeArgument(i);
|
||||
}
|
||||
call.addArguments([nameText, configObj, callbackText!]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function migratePromptCall(call: CallExpression, sourceFile: SourceFile, diagnostics: Diagnostic[]): boolean {
|
||||
const args = call.getArguments();
|
||||
if (args.length < 2) return false;
|
||||
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) return false;
|
||||
|
||||
const nameArg = args[0]!;
|
||||
if (!isStringArg(nameArg)) return false;
|
||||
const nameText = nameArg.getText();
|
||||
|
||||
let description: string | undefined;
|
||||
let schema: string | undefined;
|
||||
let callbackText: string | undefined;
|
||||
|
||||
switch (args.length) {
|
||||
case 2: {
|
||||
callbackText = args[1]!.getText();
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
const arg1 = args[1]!;
|
||||
if (isStringArg(arg1)) {
|
||||
description = arg1.getText();
|
||||
callbackText = args[2]!.getText();
|
||||
} else {
|
||||
emitWrapDiagnostic(arg1, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(arg1);
|
||||
callbackText = args[2]!.getText();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
description = args[1]!.getText();
|
||||
emitWrapDiagnostic(args[2]!, sourceFile, call, diagnostics);
|
||||
schema = maybeWrapSchema(args[2]!);
|
||||
callbackText = args[3]!.getText();
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const configParts: string[] = [];
|
||||
if (description) configParts.push(`description: ${description}`);
|
||||
if (schema) configParts.push(`argsSchema: ${schema}`);
|
||||
const configObj = configParts.length > 0 ? `{ ${configParts.join(', ')} }` : '{}';
|
||||
|
||||
expr.getNameNode().replaceWithText('registerPrompt');
|
||||
for (let i = args.length - 1; i >= 0; i--) {
|
||||
call.removeArgument(i);
|
||||
}
|
||||
call.addArguments([nameText, configObj, callbackText!]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boolean {
|
||||
const args = call.getArguments();
|
||||
if (args.length < 3) return false;
|
||||
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) return false;
|
||||
|
||||
const nameArg = args[0]!;
|
||||
if (!isStringArg(nameArg)) return false;
|
||||
const nameText = nameArg.getText();
|
||||
|
||||
const uriArg = args[1]!;
|
||||
const uriText = uriArg.getText();
|
||||
|
||||
if (args.length === 3) {
|
||||
// server.resource(name, uri, callback) → server.registerResource(name, uri, {}, callback)
|
||||
expr.getNameNode().replaceWithText('registerResource');
|
||||
const callbackText = args[2]!.getText();
|
||||
for (let i = args.length - 1; i >= 0; i--) {
|
||||
call.removeArgument(i);
|
||||
}
|
||||
call.addArguments([nameText, uriText, '{}', callbackText]);
|
||||
} else if (args.length === 4) {
|
||||
// server.resource(name, uri, metadata, callback) → server.registerResource(name, uri, metadata, callback)
|
||||
// Already has metadata, just rename the method
|
||||
expr.getNameNode().replaceWithText('registerResource');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;
|
||||
|
||||
function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
|
||||
const localName = resolveLocalImportName(sourceFile, 'McpServer');
|
||||
if (!localName) return 0;
|
||||
|
||||
let changes = 0;
|
||||
|
||||
for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
||||
if (node.wasForgotten()) continue;
|
||||
const expr = node.getExpression();
|
||||
if (!Node.isIdentifier(expr) || expr.getText() !== localName) continue;
|
||||
|
||||
const args = node.getArguments();
|
||||
if (args.length < 2) continue;
|
||||
|
||||
const optionsArg = args[1]!;
|
||||
if (!Node.isObjectLiteralExpression(optionsArg)) continue;
|
||||
|
||||
// Check if any task options are present at the top level
|
||||
const propsToMove: string[] = [];
|
||||
for (const propName of TASK_OPTIONS) {
|
||||
if (optionsArg.getProperty(propName)) {
|
||||
propsToMove.push(propName);
|
||||
}
|
||||
}
|
||||
if (propsToMove.length === 0) continue;
|
||||
|
||||
// Find the tasks object's position within the options text using AST,
|
||||
// then do all mutations via a single text replacement to avoid node invalidation.
|
||||
const capabilitiesProp = optionsArg.getProperty('capabilities');
|
||||
let tasksObjStart = -1;
|
||||
let tasksObjEnd = -1;
|
||||
const optionsStart = optionsArg.getStart();
|
||||
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
|
||||
const capInit = capabilitiesProp.getInitializer();
|
||||
if (capInit && Node.isObjectLiteralExpression(capInit)) {
|
||||
const tasksProp = capInit.getProperty('tasks');
|
||||
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
|
||||
const tasksInit = tasksProp.getInitializer();
|
||||
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
|
||||
tasksObjStart = tasksInit.getStart() - optionsStart;
|
||||
tasksObjEnd = tasksInit.getEnd() - optionsStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tasksObjStart === -1) {
|
||||
for (const propName of propsToMove) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single text replacement: remove top-level props and insert into tasks object.
|
||||
// Use AST nodes (already located via getProperty) to get brace-balanced text and
|
||||
// exact positions, avoiding regex truncation on values containing commas/braces.
|
||||
// Collect all properties first, then process in reverse position order so each
|
||||
// removal doesn't invalidate the positions of subsequent removals.
|
||||
let optionsText = optionsArg.getText();
|
||||
const argStart = optionsArg.getStart();
|
||||
const propsWithPositions: { text: string; start: number; end: number }[] = [];
|
||||
for (const propName of propsToMove) {
|
||||
const prop = optionsArg.getProperty(propName);
|
||||
if (!prop) continue;
|
||||
propsWithPositions.push({
|
||||
text: prop.getText(),
|
||||
start: prop.getStart() - argStart,
|
||||
end: prop.getEnd() - argStart
|
||||
});
|
||||
}
|
||||
const propTexts = propsWithPositions.map(p => p.text);
|
||||
|
||||
// Remove in reverse position order so earlier positions remain valid
|
||||
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
|
||||
for (const { start, end } of sortedProps) {
|
||||
let remStart = start;
|
||||
let remEnd = end;
|
||||
// Consume trailing comma and whitespace
|
||||
const afterProp = optionsText.slice(remEnd);
|
||||
const trailingMatch = afterProp.match(/^\s*,?\s*/);
|
||||
if (trailingMatch) {
|
||||
remEnd += trailingMatch[0].length;
|
||||
}
|
||||
// Consume leading whitespace/newline
|
||||
const beforeProp = optionsText.slice(0, remStart);
|
||||
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
|
||||
if (leadingMatch) {
|
||||
remStart -= leadingMatch[0].length;
|
||||
}
|
||||
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
|
||||
// Adjust tasks position if removal was before it
|
||||
if (remStart < tasksObjStart) {
|
||||
const shift = remEnd - remStart;
|
||||
tasksObjStart -= shift;
|
||||
tasksObjEnd -= shift;
|
||||
}
|
||||
}
|
||||
|
||||
if (propTexts.length === 0) continue;
|
||||
|
||||
// Insert into the tasks object (just before its closing brace)
|
||||
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
|
||||
const closingBrace = tasksText.lastIndexOf('}');
|
||||
const before = tasksText.slice(0, closingBrace).trimEnd();
|
||||
const sep = before.length > 1 ? ',\n' : '\n';
|
||||
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
|
||||
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);
|
||||
|
||||
// Clean up double/trailing commas
|
||||
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
|
||||
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');
|
||||
|
||||
optionsArg.replaceWithText(optionsText);
|
||||
changes += propTexts.length;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
325
packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts
Normal file
325
packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { v2Gap, warning } from '../../../utils/diagnostics.js';
|
||||
import { isSdkSpecifier } from '../../../utils/importUtils.js';
|
||||
import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js';
|
||||
import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js';
|
||||
import { SIMPLE_RENAMES } from '../mappings/symbolMap.js';
|
||||
|
||||
const MOCK_METHODS = new Set(['mock', 'doMock']);
|
||||
const MOCK_CALLERS = new Set(['vi', 'jest']);
|
||||
|
||||
export const mockPathsTransform: Transform = {
|
||||
name: 'Mock and dynamic import path rewrites',
|
||||
id: 'mock-paths',
|
||||
apply(sourceFile: SourceFile, context: TransformContext): TransformResult {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const usedPackages = new Set<string>();
|
||||
let changesCount = 0;
|
||||
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
for (const call of calls) {
|
||||
const expr = call.getExpression();
|
||||
|
||||
if (Node.isPropertyAccessExpression(expr)) {
|
||||
const objName = expr.getExpression().getText();
|
||||
const methodName = expr.getName();
|
||||
if (MOCK_CALLERS.has(objName) && MOCK_METHODS.has(methodName)) {
|
||||
changesCount += rewriteMockCall(call, sourceFile, context, diagnostics, usedPackages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changesCount += rewriteDynamicImports(sourceFile, context, diagnostics, usedPackages);
|
||||
|
||||
return { changesCount, diagnostics, usedPackages };
|
||||
}
|
||||
};
|
||||
|
||||
function resolveTarget(
|
||||
specifier: string,
|
||||
context: TransformContext,
|
||||
sourceFile: SourceFile,
|
||||
diagnosticSink?: { filePath: string; line: number; diagnostics: Diagnostic[] }
|
||||
):
|
||||
| { target: string; renamedSymbols?: Record<string, string>; symbolTargetOverrides?: Record<string, string> }
|
||||
| { removed: true; isV2Gap?: boolean; removalMessage?: string }
|
||||
| null {
|
||||
const mapping = IMPORT_MAP[specifier];
|
||||
if (!mapping && isAuthImport(specifier)) return { removed: true };
|
||||
if (!mapping) return null;
|
||||
if (mapping.status === 'removed') return { removed: true, isV2Gap: mapping.isV2Gap, removalMessage: mapping.removalMessage };
|
||||
|
||||
let target = mapping.target;
|
||||
if (target === 'RESOLVE_BY_CONTEXT') {
|
||||
const hasClient = sourceFile.getImportDeclarations().some(i => {
|
||||
const s = i.getModuleSpecifierValue();
|
||||
return s.includes('/client/') || s === '@modelcontextprotocol/client';
|
||||
});
|
||||
const hasServer = sourceFile.getImportDeclarations().some(i => {
|
||||
const s = i.getModuleSpecifierValue();
|
||||
return s.includes('/server/') || s === '@modelcontextprotocol/server';
|
||||
});
|
||||
target = resolveTypesPackage(context, hasClient, hasServer, diagnosticSink);
|
||||
}
|
||||
|
||||
return { target, renamedSymbols: mapping.renamedSymbols, symbolTargetOverrides: mapping.symbolTargetOverrides };
|
||||
}
|
||||
|
||||
function rewriteMockCall(
|
||||
call: import('ts-morph').CallExpression,
|
||||
sourceFile: SourceFile,
|
||||
context: TransformContext,
|
||||
diagnostics: Diagnostic[],
|
||||
usedPackages: Set<string>
|
||||
): number {
|
||||
const args = call.getArguments();
|
||||
if (args.length === 0) return 0;
|
||||
|
||||
const firstArg = args[0]!;
|
||||
if (!Node.isStringLiteral(firstArg)) return 0;
|
||||
|
||||
const specifier = firstArg.getLiteralValue();
|
||||
if (!isSdkSpecifier(specifier)) return 0;
|
||||
|
||||
const resolved = resolveTarget(specifier, context, sourceFile, {
|
||||
filePath: sourceFile.getFilePath(),
|
||||
line: call.getStartLineNumber(),
|
||||
diagnostics
|
||||
});
|
||||
if (resolved === null) {
|
||||
diagnostics.push(
|
||||
warning(sourceFile.getFilePath(), call.getStartLineNumber(), `Unknown SDK mock path: ${specifier}. Manual migration required.`)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
if ('removed' in resolved) {
|
||||
const diagFn = resolved.isV2Gap ? v2Gap : warning;
|
||||
diagnostics.push(
|
||||
diagFn(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
resolved.removalMessage ?? `Mock references removed SDK path: ${specifier}. Manual migration required.`
|
||||
)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let changes = 0;
|
||||
|
||||
let effectiveTarget = resolved.target;
|
||||
if (resolved.symbolTargetOverrides && args.length >= 2) {
|
||||
const factorySymbols = collectFactorySymbols(args[1]!);
|
||||
const allOverridden = factorySymbols.length > 0 && factorySymbols.every(s => s in resolved.symbolTargetOverrides!);
|
||||
const someOverridden = factorySymbols.some(s => s in resolved.symbolTargetOverrides!);
|
||||
if (allOverridden) {
|
||||
effectiveTarget = resolved.symbolTargetOverrides[factorySymbols[0]!]!;
|
||||
} else if (someOverridden) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
call.getStartLineNumber(),
|
||||
`Mock factory from ${specifier} mixes symbols that belong to different v2 packages. ` +
|
||||
`Split the mock manually so each symbol targets the correct package.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
usedPackages.add(effectiveTarget);
|
||||
firstArg.setLiteralValue(effectiveTarget);
|
||||
changes++;
|
||||
|
||||
const allRenames: Record<string, string> = { ...SIMPLE_RENAMES, ...resolved.renamedSymbols };
|
||||
if (args.length >= 2) {
|
||||
changes += renameSymbolsInFactory(args[1]!, allRenames);
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function getTopLevelObjectLiteral(factoryArg: import('ts-morph').Node): import('ts-morph').ObjectLiteralExpression | undefined {
|
||||
if (Node.isObjectLiteralExpression(factoryArg)) return factoryArg;
|
||||
|
||||
if (Node.isArrowFunction(factoryArg) || Node.isFunctionExpression(factoryArg)) {
|
||||
const body = factoryArg.getBody();
|
||||
if (Node.isObjectLiteralExpression(body)) return body;
|
||||
if (Node.isParenthesizedExpression(body)) {
|
||||
const inner = body.getExpression();
|
||||
if (Node.isObjectLiteralExpression(inner)) return inner;
|
||||
}
|
||||
if (Node.isBlock(body)) {
|
||||
for (const stmt of body.getStatements()) {
|
||||
if (Node.isReturnStatement(stmt)) {
|
||||
const expr = stmt.getExpression();
|
||||
if (expr && Node.isObjectLiteralExpression(expr)) return expr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectFactorySymbols(factoryArg: import('ts-morph').Node): string[] {
|
||||
const obj = getTopLevelObjectLiteral(factoryArg);
|
||||
if (!obj) return [];
|
||||
|
||||
const symbols: string[] = [];
|
||||
for (const prop of obj.getProperties()) {
|
||||
if (Node.isPropertyAssignment(prop) || Node.isShorthandPropertyAssignment(prop)) {
|
||||
symbols.push(prop.getName());
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
function renameSymbolsInFactory(factoryArg: import('ts-morph').Node, renamedSymbols: Record<string, string>): number {
|
||||
const obj = getTopLevelObjectLiteral(factoryArg);
|
||||
if (!obj) return 0;
|
||||
|
||||
let changes = 0;
|
||||
for (const prop of obj.getProperties()) {
|
||||
if (Node.isPropertyAssignment(prop)) {
|
||||
const name = prop.getName();
|
||||
const newName = renamedSymbols[name];
|
||||
if (newName) {
|
||||
prop.getNameNode().replaceWithText(newName);
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
|
||||
if (Node.isShorthandPropertyAssignment(prop)) {
|
||||
const name = prop.getName();
|
||||
const newName = renamedSymbols[name];
|
||||
if (newName) {
|
||||
prop.replaceWithText(`${newName}: ${name}`);
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function rewriteDynamicImports(
|
||||
sourceFile: SourceFile,
|
||||
context: TransformContext,
|
||||
diagnostics: Diagnostic[],
|
||||
usedPackages: Set<string>
|
||||
): number {
|
||||
let changes = 0;
|
||||
|
||||
sourceFile.forEachDescendant(node => {
|
||||
if (!Node.isCallExpression(node)) return;
|
||||
|
||||
const expr = node.getExpression();
|
||||
if (expr.getKind() !== SyntaxKind.ImportKeyword) return;
|
||||
|
||||
const args = node.getArguments();
|
||||
if (args.length === 0) return;
|
||||
|
||||
const firstArg = args[0]!;
|
||||
if (!Node.isStringLiteral(firstArg)) return;
|
||||
|
||||
const specifier = firstArg.getLiteralValue();
|
||||
if (!isSdkSpecifier(specifier)) return;
|
||||
|
||||
const resolved = resolveTarget(specifier, context, sourceFile, {
|
||||
filePath: sourceFile.getFilePath(),
|
||||
line: node.getStartLineNumber(),
|
||||
diagnostics
|
||||
});
|
||||
if (resolved === null) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
`Unknown SDK dynamic import path: ${specifier}. Manual migration required.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if ('removed' in resolved) {
|
||||
const diagFn = resolved.isV2Gap ? v2Gap : warning;
|
||||
diagnostics.push(
|
||||
diagFn(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
resolved.removalMessage ?? `Dynamic import references removed SDK path: ${specifier}. Manual migration required.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let effectiveTarget = resolved.target;
|
||||
const allRenames: Record<string, string> = { ...SIMPLE_RENAMES, ...resolved.renamedSymbols };
|
||||
|
||||
// Check if destructured symbols should route to an override target
|
||||
if (resolved.symbolTargetOverrides) {
|
||||
const parent = node.getParent();
|
||||
if (parent && Node.isAwaitExpression(parent)) {
|
||||
const grandParent = parent.getParent();
|
||||
if (grandParent && Node.isVariableDeclaration(grandParent)) {
|
||||
const nameNode = grandParent.getNameNode();
|
||||
if (Node.isObjectBindingPattern(nameNode)) {
|
||||
const elements = nameNode.getElements();
|
||||
const allOverridden =
|
||||
elements.length > 0 &&
|
||||
elements.every(el => {
|
||||
const key = el.getPropertyNameNode()?.getText() ?? el.getName();
|
||||
return key in resolved.symbolTargetOverrides!;
|
||||
});
|
||||
if (allOverridden) {
|
||||
effectiveTarget =
|
||||
resolved.symbolTargetOverrides[elements[0]!.getPropertyNameNode()?.getText() ?? elements[0]!.getName()]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usedPackages.add(effectiveTarget);
|
||||
firstArg.setLiteralValue(effectiveTarget);
|
||||
changes++;
|
||||
|
||||
const parent = node.getParent();
|
||||
if (parent && Node.isAwaitExpression(parent)) {
|
||||
const grandParent = parent.getParent();
|
||||
if (grandParent && Node.isVariableDeclaration(grandParent)) {
|
||||
const nameNode = grandParent.getNameNode();
|
||||
if (Node.isObjectBindingPattern(nameNode)) {
|
||||
for (const element of nameNode.getElements()) {
|
||||
const propertyName = element.getPropertyNameNode()?.getText();
|
||||
const bindingName = element.getName();
|
||||
const lookupKey = propertyName ?? bindingName;
|
||||
const newName = allRenames[lookupKey];
|
||||
if (newName) {
|
||||
if (propertyName) {
|
||||
element.getPropertyNameNode()!.replaceWithText(newName);
|
||||
} else {
|
||||
element.replaceWithText(`${newName}: ${bindingName}`);
|
||||
}
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const moduleRenames = resolved.renamedSymbols ?? {};
|
||||
if (!Node.isObjectBindingPattern(nameNode) && Object.keys(moduleRenames).length > 0) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
`Dynamic import assigned to variable (not destructured). Symbol renames (${Object.keys(moduleRenames).join(', ')}) were not applied. Manual update may be needed.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { renameAllReferences } from '../../../utils/astUtils.js';
|
||||
import { warning } from '../../../utils/diagnostics.js';
|
||||
import { addOrMergeImport, isAnyMcpSpecifier } from '../../../utils/importUtils.js';
|
||||
|
||||
const REMOVED_ZOD_HELPERS: Record<string, string> = {
|
||||
schemaToJson:
|
||||
"Removed in v2. Use `fromJsonSchema()` from @modelcontextprotocol/server for JSON Schema, or your schema library's native conversion.",
|
||||
parseSchemaAsync: "Removed in v2. Use your schema library's validation directly (e.g., Zod's `.safeParseAsync()`).",
|
||||
getSchemaShape: "Removed in v2. These Zod-specific introspection helpers have no v2 equivalent. Use your schema library's native API.",
|
||||
getSchemaDescription:
|
||||
"Removed in v2. These Zod-specific introspection helpers have no v2 equivalent. Use your schema library's native API.",
|
||||
isOptionalSchema:
|
||||
"Removed in v2. These Zod-specific introspection helpers have no v2 equivalent. Use your schema library's native API.",
|
||||
unwrapOptionalSchema:
|
||||
"Removed in v2. These Zod-specific introspection helpers have no v2 equivalent. Use your schema library's native API."
|
||||
};
|
||||
|
||||
export const removedApisTransform: Transform = {
|
||||
name: 'Removed API handling',
|
||||
id: 'removed-apis',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
let changesCount = 0;
|
||||
|
||||
changesCount += handleRemovedZodHelpers(sourceFile, diagnostics);
|
||||
changesCount += handleIsomorphicHeaders(sourceFile, diagnostics);
|
||||
changesCount += handleStreamableHTTPError(sourceFile, diagnostics);
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
||||
function handleRemovedZodHelpers(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
|
||||
interface Removal {
|
||||
importName: string;
|
||||
message: string;
|
||||
line: number;
|
||||
}
|
||||
|
||||
const removals: Removal[] = [];
|
||||
|
||||
for (const imp of sourceFile.getImportDeclarations()) {
|
||||
if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue;
|
||||
const line = imp.getStartLineNumber();
|
||||
for (const namedImport of imp.getNamedImports()) {
|
||||
const name = namedImport.getName();
|
||||
const message = REMOVED_ZOD_HELPERS[name];
|
||||
if (message) {
|
||||
removals.push({ importName: name, message, line });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const removal of removals) {
|
||||
for (const imp of sourceFile.getImportDeclarations()) {
|
||||
if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue;
|
||||
for (const namedImport of imp.getNamedImports()) {
|
||||
if (namedImport.getName() === removal.importName) {
|
||||
namedImport.remove();
|
||||
if (imp.getNamedImports().length === 0 && !imp.getDefaultImport() && !imp.getNamespaceImport()) {
|
||||
imp.remove();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics.push(warning(sourceFile.getFilePath(), removal.line, `${removal.importName}: ${removal.message}`));
|
||||
}
|
||||
|
||||
return removals.length;
|
||||
}
|
||||
|
||||
function handleIsomorphicHeaders(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
|
||||
let changesCount = 0;
|
||||
let foundImport: ReturnType<ReturnType<SourceFile['getImportDeclarations']>[0]['getNamedImports']>[0] | undefined;
|
||||
let foundImportDecl: ReturnType<SourceFile['getImportDeclarations']>[0] | undefined;
|
||||
|
||||
for (const imp of sourceFile.getImportDeclarations()) {
|
||||
if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue;
|
||||
for (const namedImport of imp.getNamedImports()) {
|
||||
if (namedImport.getName() === 'IsomorphicHeaders') {
|
||||
foundImport = namedImport;
|
||||
foundImportDecl = imp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundImport) break;
|
||||
}
|
||||
|
||||
if (!foundImport || !foundImportDecl) return 0;
|
||||
|
||||
const localName = foundImport.getAliasNode()?.getText() ?? 'IsomorphicHeaders';
|
||||
const line = foundImportDecl.getStartLineNumber();
|
||||
|
||||
renameAllReferences(sourceFile, localName, 'Headers');
|
||||
changesCount++;
|
||||
|
||||
foundImport.remove();
|
||||
if (foundImportDecl.getNamedImports().length === 0 && !foundImportDecl.getDefaultImport() && !foundImportDecl.getNamespaceImport()) {
|
||||
foundImportDecl.remove();
|
||||
}
|
||||
changesCount++;
|
||||
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
line,
|
||||
'IsomorphicHeaders replaced with standard Web Headers API. Note: Headers uses .get()/.set() methods, not bracket access.'
|
||||
)
|
||||
);
|
||||
|
||||
return changesCount;
|
||||
}
|
||||
|
||||
function handleStreamableHTTPError(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
|
||||
let changesCount = 0;
|
||||
let foundImport: ReturnType<ReturnType<SourceFile['getImportDeclarations']>[0]['getNamedImports']>[0] | undefined;
|
||||
let foundImportDecl: ReturnType<SourceFile['getImportDeclarations']>[0] | undefined;
|
||||
|
||||
for (const imp of sourceFile.getImportDeclarations()) {
|
||||
if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue;
|
||||
for (const namedImport of imp.getNamedImports()) {
|
||||
if (namedImport.getName() === 'StreamableHTTPError') {
|
||||
foundImport = namedImport;
|
||||
foundImportDecl = imp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundImport) break;
|
||||
}
|
||||
|
||||
if (!foundImport || !foundImportDecl) return 0;
|
||||
|
||||
const localName = foundImport.getAliasNode()?.getText() ?? 'StreamableHTTPError';
|
||||
const line = foundImportDecl.getStartLineNumber();
|
||||
const moduleSpec = foundImportDecl.getModuleSpecifierValue();
|
||||
|
||||
let hasConstructorCalls = false;
|
||||
for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
||||
const expr = node.getExpression();
|
||||
if (!Node.isIdentifier(expr) || expr.getText() !== localName) continue;
|
||||
hasConstructorCalls = true;
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
'new StreamableHTTPError(statusCode, statusText, body?) → new SdkError(code, message, data?). ' +
|
||||
'Constructor arguments differ — manual review required. Map HTTP status to SdkErrorCode enum value.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renameAllReferences(sourceFile, localName, 'SdkError');
|
||||
changesCount++;
|
||||
|
||||
foundImport.remove();
|
||||
if (foundImportDecl.getNamedImports().length === 0 && !foundImportDecl.getDefaultImport() && !foundImportDecl.getNamespaceImport()) {
|
||||
foundImportDecl.remove();
|
||||
}
|
||||
|
||||
const targetModule = resolveTargetModule(sourceFile, moduleSpec);
|
||||
const insertIndex = sourceFile.getImportDeclarations().length;
|
||||
const importsToAdd = hasConstructorCalls ? ['SdkError', 'SdkErrorCode'] : ['SdkError'];
|
||||
addOrMergeImport(sourceFile, targetModule, importsToAdd, false, insertIndex);
|
||||
changesCount++;
|
||||
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
line,
|
||||
'StreamableHTTPError replaced with SdkError. Constructor arguments differ — manual review required. ' +
|
||||
'HTTP status is now in error.data?.status.'
|
||||
)
|
||||
);
|
||||
|
||||
return changesCount;
|
||||
}
|
||||
|
||||
function resolveTargetModule(sourceFile: SourceFile, originalModule: string): string {
|
||||
const imp = sourceFile.getImportDeclarations().find(i => {
|
||||
const spec = i.getModuleSpecifierValue();
|
||||
return spec === '@modelcontextprotocol/client' || spec === '@modelcontextprotocol/server';
|
||||
});
|
||||
if (imp) return imp.getModuleSpecifierValue();
|
||||
|
||||
if (originalModule.includes('/client')) return '@modelcontextprotocol/client';
|
||||
return '@modelcontextprotocol/server';
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import type { SourceFile } from 'ts-morph';
|
||||
import { Node, SyntaxKind } from 'ts-morph';
|
||||
|
||||
import type { Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { isImportedFromMcp, removeUnusedImport, resolveOriginalImportName } from '../../../utils/importUtils.js';
|
||||
|
||||
const TARGET_METHODS = new Set(['request', 'callTool']);
|
||||
|
||||
export const schemaParamRemovalTransform: Transform = {
|
||||
name: 'Schema parameter removal',
|
||||
id: 'schema-params',
|
||||
apply(sourceFile: SourceFile, _context: TransformContext): TransformResult {
|
||||
let changesCount = 0;
|
||||
|
||||
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||||
|
||||
for (const call of calls) {
|
||||
const expr = call.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) continue;
|
||||
|
||||
const methodName = expr.getName();
|
||||
if (!TARGET_METHODS.has(methodName)) continue;
|
||||
|
||||
const args = call.getArguments();
|
||||
if (args.length < 2) continue;
|
||||
|
||||
const secondArg = args[1]!;
|
||||
if (!Node.isIdentifier(secondArg)) continue;
|
||||
|
||||
const schemaName = secondArg.getText();
|
||||
const originalName = resolveOriginalImportName(sourceFile, schemaName) ?? schemaName;
|
||||
if (!originalName.endsWith('Schema')) continue;
|
||||
if (!isImportedFromMcp(sourceFile, schemaName)) continue;
|
||||
|
||||
call.removeArgument(1);
|
||||
changesCount++;
|
||||
|
||||
removeUnusedImport(sourceFile, schemaName, true);
|
||||
}
|
||||
|
||||
return { changesCount, diagnostics: [] };
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue