fix(web): read WebMCP inputs from schema properties

Remove the fallback that interpreted schema metadata as parameter names.
Preserve exact literal property keys, including empty names, without inventing
names for missing or dynamic properties. Keep this an observed name projection.

Refs #3331, #3765

Co-authored-by: abdooo2235 <abdalrhmantheiv15@gmail.com>
This commit is contained in:
morluto
2026-10-16 06:06:07 +08:00
parent 396b89c5e2
commit ebd5a82c43
2 changed files with 52 additions and 13 deletions
+41
View File
@@ -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", () => {
+11 -13
View File
@@ -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,