diff --git a/core/audits/webmcp-schema-validity.js b/core/audits/webmcp-schema-validity.js new file mode 100644 index 000000000000..a420f5199d13 --- /dev/null +++ b/core/audits/webmcp-schema-validity.js @@ -0,0 +1,134 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Audit} from './audit.js'; +import * as i18n from '../lib/i18n/i18n.js'; + +const UIStrings = { + /** Title of a Lighthouse audit that evaluates WebMCP schema validity. This descriptive title is shown to users when there are no schema validity issues. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */ + title: 'WebMCP schemas are valid', + /** Title of a Lighthouse audit that provides detail on WebMCP schema validity. This descriptive title is shown to users when there are schema validity issues. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */ + failureTitle: 'WebMCP schemas are invalid', + /** Description of a Lighthouse audit that tells the user why they should ensure WebMCP schemas are valid. This is displayed after a user expands the section to see more. No character length limits. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */ + description: 'Valid WebMCP schemas are required for AI agents to ' + + ' understand and interact with tools correctly. ' + + 'Please fix any errors or warnings reported by the browser.', + /** Header of the table column which displays the element. */ + columnElement: 'Element', + /** Header of the table column which displays the issue. */ + columnIssue: 'Issue', + /** Descriptive reason for why a form fails WebMCP validation due to missing `toolname` attribute. */ + missingToolName: 'Form level `toolname` attribute is missing. Add it to define the tool name.', + /** Descriptive reason for why a form fails WebMCP validation due to missing `tooldescription` attribute. */ + missingToolDescription: 'Form level `tooldescription` attribute is missing. ' + + 'Add it to describe the tool for AI agents.', + /** Descriptive reason for why a form field fails WebMCP validation due to missing `name` attribute for a required field. */ + missingRequiredParamName: 'Missing `name` attribute for a required field. ' + + 'Add it to define the parameter name.', + /** Descriptive reason for why a form field fails WebMCP validation due to missing `name` attribute for an optional field. */ + missingOptionalParamName: 'Missing `name` attribute for an optional field. ' + + 'Add it to define the parameter name.', + /** Descriptive reason for why a form field fails WebMCP validation due to missing description. */ + missingParamDescription: 'Add a description to make this form more accessible for AI agents.', +}; + +const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); + +class WebMcpSchemaValidity extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'webmcp-schema-validity', + title: str_(UIStrings.title), + failureTitle: str_(UIStrings.failureTitle), + description: str_(UIStrings.description), + requiredArtifacts: ['WebMCPTools', 'WebMcpSchemaIssues'], + supportedModes: ['navigation', 'snapshot'], + }; + } + + /** + * @param {LH.Artifacts} artifacts + * @return {Promise} + */ + static async audit(artifacts) { + /** @enum {number} */ + const Severity = { + ERROR: 1, + WARNING: 2, + }; + + /** @type {Record} */ + const issueConfigs = { + 'FormModelContextMissingToolName': { + severity: Severity.ERROR, + description: str_(UIStrings.missingToolName), + }, + 'FormModelContextMissingToolDescription': { + severity: Severity.ERROR, + description: str_(UIStrings.missingToolDescription), + }, + 'FormModelContextRequiredParameterMissingName': { + severity: Severity.ERROR, + description: str_(UIStrings.missingRequiredParamName), + }, + 'FormModelContextParameterMissingTitleAndDescription': { + severity: Severity.WARNING, + description: str_(UIStrings.missingParamDescription), + }, + 'FormModelContextParameterMissingName': { + severity: Severity.WARNING, + description: str_(UIStrings.missingOptionalParamName), + }, + }; + + const rawIssues = artifacts.WebMcpSchemaIssues; + + const uniqueIssues = [ + ...new Map( + rawIssues.map(issue => [`${issue.violatingNodeId}_${issue.errorType}`, issue]) + ).values(), + ]; + const sortedUniqueIssues = uniqueIssues.sort((a, b) => { + return (issueConfigs[a.errorType]?.severity || Severity.ERROR) - + (issueConfigs[b.errorType]?.severity || Severity.ERROR); + }); + const items = sortedUniqueIssues.map(issue => { + return { + element: issue.nodeDetails ? Audit.makeNodeItem(issue.nodeDetails) : undefined, + issue: issueConfigs[issue.errorType]?.description || '', + }; + }); + + /** @type {LH.Audit.Details.Table['headings']} */ + const headings = [ + {key: 'element', valueType: 'node', label: str_(UIStrings.columnElement)}, + {key: 'issue', valueType: 'text', label: str_(UIStrings.columnIssue)}, + ]; + + const details = Audit.makeTableDetails(headings, items); + + const hasErrors = + sortedUniqueIssues.some(issue => issueConfigs[issue.errorType]?.severity === Severity.ERROR); + const hasTools = artifacts.WebMCPTools && artifacts.WebMCPTools.length > 0; + if (!hasTools && rawIssues.length === 0) { + return { + notApplicable: true, + score: 1, + }; + } + + return { + score: hasErrors ? 0 : (items.length > 0 ? 0.5 : 1), + details: items.length > 0 ? details : undefined, + }; + } +} + +export default WebMcpSchemaValidity; +export {UIStrings}; diff --git a/core/config/agentic-browsing-config.js b/core/config/agentic-browsing-config.js index a9d3c994839d..b499e19a3430 100644 --- a/core/config/agentic-browsing-config.js +++ b/core/config/agentic-browsing-config.js @@ -31,10 +31,12 @@ const config = { audits: [ 'webmcp-registered-tools', 'webmcp-form-coverage', + 'webmcp-schema-validity', 'agentic/llms-txt', ], artifacts: [ {id: 'WebMCPTools', gatherer: 'webmcp-tools'}, + {id: 'WebMcpSchemaIssues', gatherer: 'webmcp-schema'}, {id: 'LlmsTxt', gatherer: 'agentic/llms-txt'}, ], groups: { @@ -56,6 +58,7 @@ const config = { auditRefs: [ {id: 'webmcp-form-coverage', weight: 1, group: 'webmcp'}, {id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'}, + {id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'}, {id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'}, {id: 'button-name', weight: 1, group: 'agent-accessibility'}, {id: 'input-button-name', weight: 1, group: 'agent-accessibility'}, diff --git a/core/gather/gatherers/meta-elements.js b/core/gather/gatherers/meta-elements.js index eea2732c1390..3b550002c985 100644 --- a/core/gather/gatherers/meta-elements.js +++ b/core/gather/gatherers/meta-elements.js @@ -32,7 +32,7 @@ function collectMetaElements() { property: getAttribute('property'), httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined, charset: getAttribute('charset'), - node: functions.getNodeDetails(meta), + node: /** @type {LH.Artifacts.NodeDetails} */ (functions.getNodeDetails(meta)), }; }); } diff --git a/core/gather/gatherers/webmcp-schema.js b/core/gather/gatherers/webmcp-schema.js new file mode 100644 index 000000000000..21f6c4025d22 --- /dev/null +++ b/core/gather/gatherers/webmcp-schema.js @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import BaseGatherer from '../base-gatherer.js'; +import {resolveNodeIdToObjectId} from '../driver/dom.js'; +import {pageFunctions} from '../../lib/page-functions.js'; +import {ExecutionContext} from '../driver/execution-context.js'; + +class WebMcpSchemaIssues extends BaseGatherer { + /** @type {LH.Gatherer.GathererMeta} */ + meta = { + supportedModes: ['navigation', 'snapshot'], + }; + + constructor() { + super(); + /** @type {LH.Artifacts.WebMcpSchemaIssue[]} */ + this._issues = []; + this._onIssueAdded = this.onIssueAdded.bind(this); + } + + /** + * @param {Record} event + */ + onIssueAdded(event) { + const issue = event.issue; + if (!issue || issue.code !== 'GenericIssue') return; + + const details = issue.details?.genericIssueDetails; + if (!details) return; + + const errorType = details.errorType; + if (errorType && ( + errorType === 'FormModelContextMissingToolName' || + errorType === 'FormModelContextMissingToolDescription' || + errorType === 'FormModelContextRequiredParameterMissingName' || + errorType === 'FormModelContextParameterMissingTitleAndDescription' || + errorType === 'FormModelContextParameterMissingName' + )) { + this._issues.push(details); + } + } + + /** + * @param {LH.Gatherer.Context} passContext + */ + async startInstrumentation(passContext) { + const session = passContext.driver.defaultSession; + session.on('Audits.issueAdded', this._onIssueAdded); + await session.sendCommand('Audits.enable'); + } + + /** + * @param {LH.Gatherer.Context} passContext + */ + async stopInstrumentation(passContext) { + const session = passContext.driver.defaultSession; + session.off('Audits.issueAdded', this._onIssueAdded); + } + + /** + * @param {LH.Gatherer.Context} context + * @return {Promise} + */ + async getArtifact(context) { + const session = context.driver.defaultSession; + + const deps = ExecutionContext.serializeDeps([ + pageFunctions.getNodeDetails, + ]); + + const promises = this._issues.map(async (issue) => { + const processedIssue = {...issue}; + if (issue.violatingNodeId) { + try { + const objectId = await resolveNodeIdToObjectId(session, issue.violatingNodeId); + if (objectId) { + const response = await session.sendCommand('Runtime.callFunctionOn', { + objectId, + functionDeclaration: `function () { + ${deps} + return getNodeDetails(this); + }`, + returnByValue: true, + awaitPromise: true, + }); + if (response && response.result && response.result.value) { + processedIssue.nodeDetails = response.result.value; + } + } + } catch (err) { + // Ignore error + } + } + return processedIssue; + }); + + return Promise.all(promises); + } +} + +export default WebMcpSchemaIssues; diff --git a/core/gather/gatherers/webmcp-tools.js b/core/gather/gatherers/webmcp-tools.js index d922b8b0e90a..d757c5e6a31b 100644 --- a/core/gather/gatherers/webmcp-tools.js +++ b/core/gather/gatherers/webmcp-tools.js @@ -23,28 +23,6 @@ import {ExecutionContext} from '../driver/execution-context.js'; * @property {any} [stackTrace] * @property {LH.Artifacts.NodeDetails} [nodeDetails] */ - -/* global getNodeDetails */ -/* c8 ignore start */ -/** - * @param {Node} node - */ -function getNodeDetailsData(node) { - /** @type {Element|null} */ - let elem = node instanceof Element ? node : node.parentElement; - if (!elem && node instanceof ShadowRoot) { - elem = node.host; - } - - let traceElement; - if (elem) { - // @ts-expect-error - getNodeDetails put into scope via stringification - traceElement = {node: getNodeDetails(elem)}; - } - return traceElement; -} -/* c8 ignore stop */ - class WebMCPTools extends BaseGatherer { /** @type {LH.Gatherer.GathererMeta} */ meta = { @@ -133,19 +111,18 @@ class WebMCPTools extends BaseGatherer { if (objectId) { const deps = ExecutionContext.serializeDeps([ pageFunctions.getNodeDetails, - getNodeDetailsData, ]); const response = await session.sendCommand('Runtime.callFunctionOn', { objectId, functionDeclaration: `function () { ${deps} - return getNodeDetailsData(this); + return getNodeDetails(this); }`, returnByValue: true, awaitPromise: true, }); if (response && response.result && response.result.value) { - tool.nodeDetails = response.result.value.node; + tool.nodeDetails = response.result.value; } } } catch (err) { diff --git a/core/lib/page-functions.js b/core/lib/page-functions.js index de6a1d2d347d..cc71266d7544 100644 --- a/core/lib/page-functions.js +++ b/core/lib/page-functions.js @@ -450,16 +450,23 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) { } /** - * @param {Element|ShadowRoot} element - * @return {LH.Artifacts.NodeDetails} + * @param {Node|ShadowRoot} node + * @return {LH.Artifacts.NodeDetails | null} */ -function getNodeDetails(element) { +function getNodeDetails(node) { // This bookkeeping is for the FullPageScreenshot gatherer. if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) { window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = new Map(); } - element = element instanceof ShadowRoot ? element.host : element; + let elem = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; + if (!elem && node instanceof ShadowRoot) { + elem = node.host; + } + + if (!elem) return null; + + const element = /** @type {Element} */ (elem); const selector = getNodeSelector(element); // Create an id that will be unique across all execution contexts. diff --git a/core/scripts/i18n/collect-strings.js b/core/scripts/i18n/collect-strings.js index 32b82f6cd04d..f4bbfb2711e3 100644 --- a/core/scripts/i18n/collect-strings.js +++ b/core/scripts/i18n/collect-strings.js @@ -739,6 +739,7 @@ function checkKnownFixedCollisions(strings) { 'Document has a valid $MARKDOWN_SNIPPET_0$', 'Element', 'Element', + 'Element', 'Est Savings', 'Est Savings', 'Failing Elements', diff --git a/core/test/audits/webmcp-schema-validity-test.js b/core/test/audits/webmcp-schema-validity-test.js new file mode 100644 index 000000000000..6a5fe1eef065 --- /dev/null +++ b/core/test/audits/webmcp-schema-validity-test.js @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert/strict'; + +import WebMcpSchemaValidityAudit from '../../audits/webmcp-schema-validity.js'; + +describe('WebMcpSchemaValidity audit', () => { + it('passes when no issues were found (not applicable)', async () => { + const auditResult = await WebMcpSchemaValidityAudit.audit({ + WebMCPTools: [], + WebMcpSchemaIssues: [], + }, {}); + assert.equal(auditResult.score, 1); + assert.equal(auditResult.notApplicable, true); + }); + + it('passes when valid tools are found without issues', async () => { + const auditResult = await WebMcpSchemaValidityAudit.audit({ + WebMCPTools: [{name: 'tool1'}], + WebMcpSchemaIssues: [], + }, {}); + assert.equal(auditResult.score, 1); + assert.equal(auditResult.notApplicable, undefined); + }); + + + it('fails when WebMCP issues are found', async () => { + const auditResult = await WebMcpSchemaValidityAudit.audit({ + WebMcpSchemaIssues: [ + { + errorType: 'FormModelContextParameterMissingTitleAndDescription', + violatingNodeId: 1, + nodeDetails: {nodeName: 'INPUT', selector: '#input1'}, + }, + { + errorType: 'FormModelContextMissingToolName', + violatingNodeId: 2, + nodeDetails: {nodeName: 'FORM', selector: '#form1'}, + }, + ], + }, {}); + + assert.equal(auditResult.score, 0); + assert.equal(auditResult.details.items.length, 2); + assert.equal(auditResult.details.items[0].issue.formattedDefault, + 'Form level `toolname` attribute is missing. Add it to define the tool name.'); + assert.equal(auditResult.details.items[1].issue.formattedDefault, + 'Add a description to make this form more accessible for AI agents.'); + }); + + it('deduplicates identical issues on the same node', async () => { + const auditResult = await WebMcpSchemaValidityAudit.audit({ + WebMcpSchemaIssues: [ + { + errorType: 'FormModelContextParameterMissingTitleAndDescription', + violatingNodeId: 1, + nodeDetails: {nodeName: 'INPUT', selector: '#input1'}, + }, + { + errorType: 'FormModelContextParameterMissingTitleAndDescription', + violatingNodeId: 1, + nodeDetails: {nodeName: 'INPUT', selector: '#input1'}, + }, + { + errorType: 'FormModelContextParameterMissingName', + violatingNodeId: 1, + nodeDetails: {nodeName: 'INPUT', selector: '#input1'}, + }, + ], + }, {}); + + assert.equal(auditResult.score, 0.5); + assert.equal(auditResult.details.items.length, 2); + assert.equal(auditResult.details.items[0].issue.formattedDefault, + 'Add a description to make this form more accessible for AI agents.'); + assert.equal(auditResult.details.items[1].issue.formattedDefault, + 'Missing `name` attribute for an optional field. Add it to define the parameter name.'); + }); +}); diff --git a/core/test/gather/gatherers/webmcp-schema-test.js b/core/test/gather/gatherers/webmcp-schema-test.js new file mode 100644 index 000000000000..8638408b9ffc --- /dev/null +++ b/core/test/gather/gatherers/webmcp-schema-test.js @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert/strict'; + +import WebMcpSchemaIssues from '../../../gather/gatherers/webmcp-schema.js'; + +describe('WebMcpSchemaIssues Gatherer', () => { + it('collects WebMCP issues and resolves node IDs', async () => { + const gatherer = new WebMcpSchemaIssues(); + + // Mock the session + const mockSession = /** @type {any} */ ({ + on: (/** @type {string} */ event, /** @type {Function} */ handler) => { + // Store the handler to simulate events + if (event === 'Audits.issueAdded') { + mockSession._issueHandler = handler; + } + }, + off: () => {}, + sendCommand: async (/** @type {string} */ command, /** @type {any} */ _) => { + if (command === 'Audits.enable') return {}; + if (command === 'DOM.resolveNode') return {object: {objectId: 'obj1'}}; + if (command === 'Runtime.callFunctionOn') { + return { + result: { + value: { + nodeName: 'INPUT', + selector: '#my-input', + }, + }, + }; + } + }, + }); + + // Helper to resolve node ID to object ID (mocked as returning true for simplify) + // In the real gatherer it calls resolveNodeIdToObjectId which calls DOM.resolveNode. + // We need to mock that or the function it calls! + // Let's assume resolveNodeIdToObjectId works if sendCommand handles it! + + await gatherer.startInstrumentation( + /** @type {any} */ ({driver: {defaultSession: mockSession}})); + + // Simulate issue added + mockSession._issueHandler({ + issue: { + code: 'GenericIssue', + details: { + genericIssueDetails: { + errorType: 'FormModelContextParameterMissingTitleAndDescription', + violatingNodeId: 123, + }, + }, + }, + }); + + const artifact = await gatherer.getArtifact( + /** @type {any} */ ({driver: {defaultSession: mockSession}})); + + assert.equal(artifact.length, 1); + assert.equal(artifact[0].errorType, 'FormModelContextParameterMissingTitleAndDescription'); + assert.deepEqual(artifact[0].nodeDetails, { + nodeName: 'INPUT', + selector: '#my-input', + }); + }); + + it('ignores unrelated issues', async () => { + const gatherer = new WebMcpSchemaIssues(); + + const mockSession = /** @type {any} */ ({ + on: (/** @type {string} */ event, /** @type {Function} */ handler) => { + if (event === 'Audits.issueAdded') { + mockSession._issueHandler = handler; + } + }, + off: () => {}, + sendCommand: async () => ({}), + }); + + await gatherer.startInstrumentation( + /** @type {any} */ ({driver: {defaultSession: mockSession}})); + + mockSession._issueHandler({ + issue: { + code: 'GenericIssue', + details: { + genericIssueDetails: { + errorType: 'UnrelatedError', + violatingNodeId: 123, + }, + }, + }, + }); + + const artifact = await gatherer.getArtifact( + /** @type {any} */ ({driver: {defaultSession: mockSession}})); + + assert.equal(artifact.length, 0); + }); +}); diff --git a/core/test/gather/gatherers/webmcp-tools-test.js b/core/test/gather/gatherers/webmcp-tools-test.js index 90042659720c..55ebe724c205 100644 --- a/core/test/gather/gatherers/webmcp-tools-test.js +++ b/core/test/gather/gatherers/webmcp-tools-test.js @@ -188,7 +188,7 @@ describe('WebMCPTools Gatherer', () => { .mockResponse('WebMCP.enable') .mockResponse('DOM.resolveNode', {object: {objectId: 'remote-obj-1'}}) .mockResponse('Runtime.callFunctionOn', - {result: {value: {node: {snippet: '
', selector: 'div'}}}}) + {result: {value: {snippet: '
', selector: 'form'}}}) .mockResponse('WebMCP.disable'); await gatherer.startInstrumentation(mockContext.asContext()); @@ -199,6 +199,6 @@ describe('WebMCPTools Gatherer', () => { assert.equal(artifact.length, 1); assert.equal(artifact[0].name, 'declarative_tool'); - assert.deepEqual(artifact[0].nodeDetails, {snippet: '
', selector: 'div'}); + assert.deepEqual(artifact[0].nodeDetails, {snippet: '
', selector: 'form'}); }); }); diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json index 1001478138c9..8e01a9c1a183 100644 --- a/shared/localization/locales/en-US.json +++ b/shared/localization/locales/en-US.json @@ -1364,6 +1364,36 @@ "core/audits/webmcp-registered-tools.js | titleImperativeTools": { "message": "Imperative Tools" }, + "core/audits/webmcp-schema-validity.js | columnElement": { + "message": "Element" + }, + "core/audits/webmcp-schema-validity.js | columnIssue": { + "message": "Issue" + }, + "core/audits/webmcp-schema-validity.js | description": { + "message": "Valid WebMCP schemas are required for AI agents to understand and interact with tools correctly. Please fix any errors or warnings reported by the browser." + }, + "core/audits/webmcp-schema-validity.js | failureTitle": { + "message": "WebMCP schemas are invalid" + }, + "core/audits/webmcp-schema-validity.js | missingOptionalParamName": { + "message": "Missing `name` attribute for an optional field. Add it to define the parameter name." + }, + "core/audits/webmcp-schema-validity.js | missingParamDescription": { + "message": "Add a description to make this form more accessible for AI agents." + }, + "core/audits/webmcp-schema-validity.js | missingRequiredParamName": { + "message": "Missing `name` attribute for a required field. Add it to define the parameter name." + }, + "core/audits/webmcp-schema-validity.js | missingToolDescription": { + "message": "Form level `tooldescription` attribute is missing. Add it to describe the tool for AI agents." + }, + "core/audits/webmcp-schema-validity.js | missingToolName": { + "message": "Form level `toolname` attribute is missing. Add it to define the tool name." + }, + "core/audits/webmcp-schema-validity.js | title": { + "message": "WebMCP schemas are valid" + }, "core/config/agentic-browsing-config.js | agentAccessibilityGroupDescription": { "message": "These audits highlight best practices for improving the accessibility of the website for AI agents." }, diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json index 7d35150d1f7b..719582a17aa7 100644 --- a/shared/localization/locales/en-XL.json +++ b/shared/localization/locales/en-XL.json @@ -1364,6 +1364,36 @@ "core/audits/webmcp-registered-tools.js | titleImperativeTools": { "message": "Îḿp̂ér̂át̂ív̂é T̂óôĺŝ" }, + "core/audits/webmcp-schema-validity.js | columnElement": { + "message": "Êĺêḿêńt̂" + }, + "core/audits/webmcp-schema-validity.js | columnIssue": { + "message": "Îśŝúê" + }, + "core/audits/webmcp-schema-validity.js | description": { + "message": "V̂ál̂íd̂ Ẃêb́M̂ĆP̂ śĉh́êḿâś âŕê ŕêq́ûír̂éd̂ f́ôŕ ÂÍ âǵêńt̂ś t̂ó ûńd̂ér̂śt̂án̂d́ âńd̂ ín̂t́êŕâćt̂ ẃît́ĥ t́ôól̂ś ĉór̂ŕêćt̂ĺŷ. Ṕl̂éâśê f́îx́ âńŷ ér̂ŕôŕŝ ór̂ ẃâŕn̂ín̂ǵŝ ŕêṕôŕt̂éd̂ b́ŷ t́ĥé b̂ŕôẃŝér̂." + }, + "core/audits/webmcp-schema-validity.js | failureTitle": { + "message": "Ŵéb̂ḾĈṔ ŝćĥém̂áŝ ár̂é îńv̂ál̂íd̂" + }, + "core/audits/webmcp-schema-validity.js | missingOptionalParamName": { + "message": "M̂íŝśîńĝ `name` át̂t́r̂íb̂út̂é f̂ór̂ án̂ óp̂t́îón̂ál̂ f́îél̂d́. Âd́d̂ ít̂ t́ô d́êf́îńê t́ĥé p̂ár̂ám̂ét̂ér̂ ńâḿê." + }, + "core/audits/webmcp-schema-validity.js | missingParamDescription": { + "message": "Âd́d̂ á d̂éŝćr̂íp̂t́îón̂ t́ô ḿâḱê t́ĥíŝ f́ôŕm̂ ḿôŕê áĉćêśŝíb̂ĺê f́ôŕ ÂÍ âǵêńt̂ś." + }, + "core/audits/webmcp-schema-validity.js | missingRequiredParamName": { + "message": "M̂íŝśîńĝ `name` át̂t́r̂íb̂út̂é f̂ór̂ á r̂éq̂úîŕêd́ f̂íêĺd̂. Ád̂d́ ît́ t̂ó d̂éf̂ín̂é t̂h́ê ṕâŕâḿêt́êŕ n̂ám̂é." + }, + "core/audits/webmcp-schema-validity.js | missingToolDescription": { + "message": "F̂ór̂ḿ l̂év̂él̂ `tooldescription` át̂t́r̂íb̂út̂é îś m̂íŝśîńĝ. Ád̂d́ ît́ t̂ó d̂éŝćr̂íb̂é t̂h́ê t́ôól̂ f́ôŕ ÂÍ âǵêńt̂ś." + }, + "core/audits/webmcp-schema-validity.js | missingToolName": { + "message": "F̂ór̂ḿ l̂év̂él̂ `toolname` át̂t́r̂íb̂út̂é îś m̂íŝśîńĝ. Ád̂d́ ît́ t̂ó d̂éf̂ín̂é t̂h́ê t́ôól̂ ńâḿê." + }, + "core/audits/webmcp-schema-validity.js | title": { + "message": "Ŵéb̂ḾĈṔ ŝćĥém̂áŝ ár̂é v̂ál̂íd̂" + }, "core/config/agentic-browsing-config.js | agentAccessibilityGroupDescription": { "message": "T̂h́êśê áûd́ît́ŝ h́îǵĥĺîǵĥt́ b̂éŝt́ p̂ŕâćt̂íĉéŝ f́ôŕ îḿp̂ŕôv́îńĝ t́ĥé âćĉéŝśîb́îĺît́ŷ óf̂ t́ĥé ŵéb̂śît́ê f́ôŕ ÂÍ âǵêńt̂ś." }, diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 1963cfa64352..cda3855be9fd 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -124,6 +124,8 @@ export interface GathererArtifacts extends PublicGathererArtifacts { InspectorIssues: Artifacts.InspectorIssues; /** The tools registered via WebMCP. */ WebMCPTools: Artifacts.WebMCPTool[]; + /** The WebMCP schema validation issues. */ + WebMcpSchemaIssues: Artifacts.WebMcpSchemaIssue[]; /** JS coverage information for code used during audit. Keyed by script id. */ // 'url' is excluded because it can be overridden by a magic sourceURL= comment, which makes keeping it a dangerous footgun! JsUsage: Record>; @@ -146,6 +148,16 @@ export interface GathererArtifacts extends PublicGathererArtifacts { } declare module Artifacts { + interface WebMcpSchemaIssue { + errorType: string; + violatingNodeId?: number; + nodeDetails?: NodeDetails; + formToolName?: string | null; + formToolDescription?: string | null; + paramName?: string | null; + paramDescription?: string | null; + } + type ComputedContext = Util.Immutable<{ computedCache: Map; }>;