feat: add v2 codemod draft (#1950)

This commit is contained in:
Konstantin Konstantinov 2026-05-21 16:13:33 +03:00 • committed by GitHub
commit 48251fe2cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 9239 additions and 1 deletions

View 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
View 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();

View 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);
}

View 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'
};

View 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';

View 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;
}

View 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
};

View file

@ -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';

View 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.');
}

View file

@ -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'
};

View file

@ -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']);

View file

@ -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 };
}
};

View file

@ -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;
}

View file

@ -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 };
}
};

View file

@ -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;
}

View 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
];

View file

@ -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;
}

View 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;
}

View file

@ -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';
}

View file

@ -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