diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index 3361cc366..9120a73d9 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -714,3 +714,54 @@ it("keeps RPC lexical identity and bundled factory recognition in captured scrip ]); expect(result.completeness.status).toBe("complete"); }); + +it("retains distinct WebMCP descriptions and names while deduplicating equivalent properties", () => { + const result = analyzeCapturedWebBundle( + inspection(` + document.modelContext.registerTool({ name: "lookup", description: "first", inputSchema: { properties: { alpha: {} } } }); + document.modelContext.registerTool({ name: "lookup", description: "second", inputSchema: { properties: { alpha: {} } } }); + document.modelContext.registerTool({ name: "equivalent", inputSchema: { properties: { alpha: {}, beta: {} } } }); + document.modelContext.registerTool({ name: "equivalent", inputSchema: { properties: { beta: {}, alpha: {}, alpha: {} } } }); + document.modelContext.registerTool({ description: "unnamed" }); + document.modelContext.registerTool({ name: "", description: "unnamed" }); + `), + ); + + expect( + result.observations.webmcp_declarations.map( + ({ name, description, schema_property_names }) => ({ + name, + description, + schema_property_names, + }), + ), + ).toEqual([ + { + name: "lookup", + description: "first", + schema_property_names: ["alpha"], + }, + { + name: "lookup", + description: "second", + schema_property_names: ["alpha"], + }, + { + name: "equivalent", + description: null, + schema_property_names: ["alpha", "beta"], + }, + { name: null, description: "unnamed", schema_property_names: [] }, + { name: "", description: "unnamed", schema_property_names: [] }, + ]); + expect(result.observations.webmcp_declarations[2]?.location).toEqual({ + script_key: `scr_${"1".repeat(64)}`, + line: 4, + column: 6, + }); + expect( + result.observations.webmcp_declarations.every( + ({ trust }) => trust === "page-declared-untrusted", + ), + ).toBe(true); +}); diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index fd8c541ac..522f22caa 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -293,19 +293,18 @@ const addWebMcpDeclaration = ( : [], ) : []; + const metadata = { + name: name ?? null, + description: description ?? null, + schema_property_names: [...new Set(schemaPropertyNames)].sort(), + }; + // Deduplicate the metadata we return, preserving omission and descriptions. // Property names are a variable-length source-derived list: NUL-joins // would collapse ["a\0b"] with ["a", "b"] and drop a declaration. - const key = compositeKey([ - "webmcp", - script.script_key, - name ?? "", - schemaPropertyNames, - ]); + const key = compositeKey(["webmcp", script.script_key, metadata]); addUnique(accumulator, key, () => accumulator.webMcp.push({ - name: name ?? null, - description: description ?? null, - schema_property_names: [...new Set(schemaPropertyNames)].sort(), + ...metadata, trust: "page-declared-untrusted", location: location(script.script_key, node), }),