fix(codemod): keep request() result schemas the call still needs (#2383)
This commit is contained in:
parent
ba27d201e1
commit
9f8ba61279
1 changed files with 157 additions and 0 deletions
|
|
@ -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++;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue