fix(codemod): keep request() result schemas the call still needs (#2383)

This commit is contained in:
Felix Weinberger 2026-06-29 15:00:39 +01:00 • committed by GitHub
commit 9f8ba61279
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,9 +3,40 @@ import { Node, SyntaxKind } from 'ts-morph';
import type { Transform, TransformContext, TransformResult } from '../../../types';
import { hasMcpImports, isImportedFromMcp, removeUnusedImport, resolveOriginalImportName } from '../../../utils/importUtils';
import { SCHEMA_TO_METHOD } from '../mappings/schemaToMethodMap';
const TARGET_METHODS = new Set(['request', 'callTool']);
/** Spec request methods — the only methods whose result schema v2 resolves by name. */
const SPEC_REQUEST_METHODS: ReadonlySet<string> = new Set(Object.values(SCHEMA_TO_METHOD));
// `request()` keeps its result-schema parameter in v2 (custom methods, passthrough
// forwarding); only calls whose method the codemod can PROVE is a literal spec method
// may safely lose it. Schema-less v2 `request()` enforces the spec result schema for
// spec methods and throws a TypeError for non-spec methods, so dropping the schema
// from a dynamic-method call (`request({ method, params }, schema)` in a
// proxy/forwarder) or from a custom-method call breaks the call site.
function literalMethodOf(arg: Node): string | undefined {
if (!Node.isObjectLiteralExpression(arg)) return undefined;
const prop = arg.getProperty('method');
if (!prop || !Node.isPropertyAssignment(prop)) return undefined;
// A spread after the `method` property can override it at runtime — not provably literal.
const props = arg.getProperties();
const spreadAfterMethod = props.slice(props.indexOf(prop) + 1).some(p => Node.isSpreadAssignment(p));
if (spreadAfterMethod) return undefined;
let initializer = prop.getInitializer();
// Unwrap `'tools/call' as const` / `satisfies` / parenthesized forms.
while (
initializer !== undefined &&
(Node.isAsExpression(initializer) || Node.isSatisfiesExpression(initializer) || Node.isParenthesizedExpression(initializer))
) {
initializer = initializer.getExpression();
}
if (initializer === undefined) return undefined;
if (!Node.isStringLiteral(initializer) && !Node.isNoSubstitutionTemplateLiteral(initializer)) return undefined;
return initializer.getLiteralValue();
}
export const schemaParamRemovalTransform: Transform = {
name: 'Schema parameter removal',
id: 'schema-params',
@ -50,6 +81,18 @@ export const schemaParamRemovalTransform: Transform = {
if (!originalName.endsWith('Schema')) continue;
if (!isImportedFromMcp(sourceFile, schemaName)) continue;
// v2 `callTool()` has no schema parameter at all, so the argument always goes.
// v2 `request()` still accepts one — only drop it when the method is a literal
// SPEC method (schema-less request() throws a TypeError for anything else) and
// the schema is method-specific: the generic `ResultSchema` is the v1
// passthrough idiom, and dropping it would silently switch the call from
// passthrough to spec-schema enforcement.
if (methodName === 'request') {
const literal = literalMethodOf(args[0]!);
if (literal === undefined || !SPEC_REQUEST_METHODS.has(literal)) continue;
if (originalName === 'ResultSchema') continue;
}
call.removeArgument(1);
changesCount++;