diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index cc137be20e..dfc3ce4cbe 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -1034,6 +1034,47 @@ describe("web metadata key semantics", () => { expect.objectContaining({ name: "valid" }), ]); }); + + it("projects only exact input schema property names", () => { + const result = analyzeCapturedWebBundle( + inspection(` + document.modelContext.registerTool({ + name: "literal", + inputSchema: { + type: "object", + properties: { type: {}, properties: {}, "": {}, [dynamicKey]: {}, ...dynamicProperties } + } + }); + document.modelContext.registerTool({ name: "missing properties", inputSchema: { type: "object" } }); + document.modelContext.registerTool({ name: "dynamic properties", inputSchema: { properties: dynamicProperties } }); + document.modelContext.registerTool({ name: "null properties", inputSchema: { properties: null } }); + document.modelContext.registerTool({ name: "missing schema" }); + document.modelContext.registerTool({ name: "dynamic schema", inputSchema: schema }); + document.modelContext.registerTool({ name: "null schema", inputSchema: null }); + document.modelContext.registerTool({ name: "spread schema", inputSchema: { ...schema } }); + document.modelContext.registerTool({ + name: "computed literal keys", + ["inputSchema"]: { ["properties"]: { [""]: {}, ["literal"]: {}, [dynamicKey]: {} } } + }); + `), + ); + + expect( + result.observations.webmcp_declarations.map( + ({ name, schema_property_names }) => ({ name, schema_property_names }), + ), + ).toEqual([ + { name: "literal", schema_property_names: ["", "properties", "type"] }, + { name: "missing properties", schema_property_names: [] }, + { name: "dynamic properties", schema_property_names: [] }, + { name: "null properties", schema_property_names: [] }, + { name: "missing schema", schema_property_names: [] }, + { name: "dynamic schema", schema_property_names: [] }, + { name: "null schema", schema_property_names: [] }, + { name: "spread schema", schema_property_names: [] }, + { name: "computed literal keys", schema_property_names: ["", "literal"] }, + ]); + }); }); it("retains WebMCP declarations through optional member and call chaining", () => { diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 9fd7599976..d28bf76e12 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -8,7 +8,10 @@ import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal. import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js"; import { completeSemanticSteps } from "./javascript/javascriptSemanticTraversal.js"; import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js"; -import { semanticStaticPropertyName } from "./javascript/javascriptAstValues.js"; +import { + semanticStaticPropertyKey, + semanticStaticPropertyName, +} from "./javascript/javascriptAstValues.js"; import { calleeName, endpointArgument, @@ -315,20 +318,15 @@ const addWebMcpDeclaration = ( const schema = objectValue(declaration, "inputSchema") ?? objectValue(declaration, "input_schema"); - const declaredProperties = t.isObjectExpression(schema) + const properties = t.isObjectExpression(schema) ? objectValue(schema, "properties") : undefined; - const propertyObject = t.isObjectExpression(declaredProperties) - ? declaredProperties - : schema; - const schemaPropertyNames = t.isObjectExpression(propertyObject) - ? propertyObject.properties.flatMap((property) => - t.isObjectProperty(property) - ? [ - semanticStaticPropertyName(property.key, property.computed), - ].filter(Boolean) - : [], - ) + const schemaPropertyNames = t.isObjectExpression(properties) + ? properties.properties.flatMap((property) => { + if (!t.isObjectProperty(property)) return []; + const key = semanticStaticPropertyKey(property.key, property.computed); + return key === null ? [] : [key]; + }) : []; const metadata = { name: name ?? null,