lighthouse 13.1.0 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/cli/bin.js +5 -0
  2. package/cli/cli-flags.js +2 -2
  3. package/cli/test/smokehouse/lighthouse-runners/bundle.js +7 -3
  4. package/cli/test/smokehouse/lighthouse-runners/cli.js +5 -1
  5. package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +7 -3
  6. package/cli/test/smokehouse/lighthouse-runners/devtools.js +4 -1
  7. package/cli/test/smokehouse/smokehouse.js +7 -2
  8. package/core/audits/accessibility/autocomplete-valid.d.ts +10 -0
  9. package/core/audits/accessibility/autocomplete-valid.js +44 -0
  10. package/core/audits/accessibility/presentation-role-conflict.d.ts +10 -0
  11. package/core/audits/accessibility/presentation-role-conflict.js +46 -0
  12. package/core/audits/accessibility/svg-img-alt.d.ts +10 -0
  13. package/core/audits/accessibility/svg-img-alt.js +44 -0
  14. package/core/audits/agentic/agent-accessibility-tree.d.ts +19 -0
  15. package/core/audits/agentic/agent-accessibility-tree.js +115 -0
  16. package/core/audits/agentic/llms-txt.d.ts +20 -0
  17. package/core/audits/agentic/llms-txt.js +111 -0
  18. package/core/audits/insights/insight-audit.d.ts +2 -2
  19. package/core/audits/insights/insight-audit.js +16 -6
  20. package/core/audits/layout-shifts.js +1 -1
  21. package/core/audits/server-response-time.js +3 -3
  22. package/core/audits/webmcp-form-coverage.d.ts +16 -0
  23. package/core/audits/webmcp-form-coverage.js +90 -0
  24. package/core/audits/webmcp-registered-tools.d.ts +21 -0
  25. package/core/audits/webmcp-registered-tools.js +149 -0
  26. package/core/audits/webmcp-schema-validity.d.ts +22 -0
  27. package/core/audits/webmcp-schema-validity.js +141 -0
  28. package/core/computed/document-urls.js +0 -1
  29. package/core/computed/main-resource.js +0 -2
  30. package/core/computed/metrics/lantern-metric.js +4 -4
  31. package/core/computed/metrics/lcp-breakdown.js +1 -1
  32. package/core/computed/metrics/time-to-first-byte.js +1 -1
  33. package/core/computed/navigation-insights.js +2 -1
  34. package/core/computed/network-analysis.js +0 -1
  35. package/core/config/agentic-browsing-config.d.ts +12 -0
  36. package/core/config/agentic-browsing-config.js +73 -0
  37. package/core/config/default-config.js +51 -0
  38. package/core/gather/gatherers/accessibility.js +5 -1
  39. package/core/gather/gatherers/agentic/llms-txt.d.ts +10 -0
  40. package/core/gather/gatherers/agentic/llms-txt.js +28 -0
  41. package/core/gather/gatherers/inputs.js +2 -0
  42. package/core/gather/gatherers/meta-elements.js +1 -1
  43. package/core/gather/gatherers/trace-elements.js +1 -1
  44. package/core/gather/gatherers/webmcp-schema.d.ts +25 -0
  45. package/core/gather/gatherers/webmcp-schema.js +105 -0
  46. package/core/gather/gatherers/webmcp.d.ts +58 -0
  47. package/core/gather/gatherers/webmcp.js +159 -0
  48. package/core/lib/baseline/web-features-metadata.json +1 -1
  49. package/core/lib/cdt/generated/SourceMap.js +2 -2
  50. package/core/lib/deprecations-strings.d.ts +26 -20
  51. package/core/lib/deprecations-strings.js +7 -0
  52. package/core/lib/navigation-error.js +0 -6
  53. package/core/lib/network-request.js +0 -1
  54. package/core/lib/page-functions.d.ts +3 -3
  55. package/core/lib/page-functions.js +11 -4
  56. package/core/lib/tracehouse/trace-processor.d.ts +5 -4
  57. package/core/lib/tracehouse/trace-processor.js +85 -19
  58. package/core/runner.js +3 -0
  59. package/core/scoring.d.ts +1 -0
  60. package/dist/report/bundle.esm.js +1 -1
  61. package/dist/report/flow.js +3 -3
  62. package/dist/report/standalone.js +1 -1
  63. package/flow-report/src/summary/category.tsx +1 -1
  64. package/package.json +11 -11
  65. package/report/renderer/category-renderer.js +1 -1
  66. package/report/renderer/report-utils.d.ts +2 -1
  67. package/report/renderer/report-utils.js +7 -2
  68. package/shared/localization/locales/ar-XB.json +72 -36
  69. package/shared/localization/locales/ar.json +72 -36
  70. package/shared/localization/locales/bg.json +72 -36
  71. package/shared/localization/locales/ca.json +72 -36
  72. package/shared/localization/locales/cs.json +72 -36
  73. package/shared/localization/locales/da.json +74 -38
  74. package/shared/localization/locales/de.json +72 -36
  75. package/shared/localization/locales/el.json +73 -37
  76. package/shared/localization/locales/en-GB.json +74 -38
  77. package/shared/localization/locales/en-US.json +263 -17
  78. package/shared/localization/locales/en-XL.json +263 -17
  79. package/shared/localization/locales/es-419.json +72 -36
  80. package/shared/localization/locales/es.json +73 -37
  81. package/shared/localization/locales/fi.json +72 -36
  82. package/shared/localization/locales/fil.json +74 -38
  83. package/shared/localization/locales/fr.json +162 -126
  84. package/shared/localization/locales/he.json +74 -38
  85. package/shared/localization/locales/hi.json +73 -37
  86. package/shared/localization/locales/hr.json +72 -36
  87. package/shared/localization/locales/hu.json +73 -37
  88. package/shared/localization/locales/id.json +74 -38
  89. package/shared/localization/locales/it.json +72 -36
  90. package/shared/localization/locales/ja.json +72 -36
  91. package/shared/localization/locales/ko.json +72 -36
  92. package/shared/localization/locales/lt.json +72 -36
  93. package/shared/localization/locales/lv.json +72 -36
  94. package/shared/localization/locales/nl.json +73 -37
  95. package/shared/localization/locales/no.json +72 -36
  96. package/shared/localization/locales/pl.json +72 -36
  97. package/shared/localization/locales/pt-PT.json +72 -36
  98. package/shared/localization/locales/pt.json +74 -38
  99. package/shared/localization/locales/ro.json +72 -36
  100. package/shared/localization/locales/ru.json +72 -36
  101. package/shared/localization/locales/sk.json +72 -36
  102. package/shared/localization/locales/sl.json +72 -36
  103. package/shared/localization/locales/sr-Latn.json +73 -37
  104. package/shared/localization/locales/sr.json +73 -37
  105. package/shared/localization/locales/sv.json +75 -39
  106. package/shared/localization/locales/ta.json +73 -37
  107. package/shared/localization/locales/te.json +72 -36
  108. package/shared/localization/locales/th.json +73 -37
  109. package/shared/localization/locales/tr.json +72 -36
  110. package/shared/localization/locales/uk.json +72 -36
  111. package/shared/localization/locales/vi.json +74 -38
  112. package/shared/localization/locales/zh-HK.json +72 -36
  113. package/shared/localization/locales/zh-TW.json +74 -38
  114. package/shared/localization/locales/zh.json +75 -39
  115. package/types/artifacts.d.ts +33 -0
  116. package/types/config.d.ts +1 -0
  117. package/types/internal/smokehouse.d.ts +7 -1
  118. package/types/lhr/lhr.d.ts +11 -0
@@ -100,6 +100,21 @@ const UIStrings = {
100
100
  bestPracticesBrowserCompatGroupTitle: 'Browser Compatibility',
101
101
  /** Title of the General group of the Best Practices category. Within this section are the audits that don't belong to a specific group but are of general interest. */
102
102
  bestPracticesGeneralGroupTitle: 'General',
103
+ /** Title of the Agentic Browsing category of audits. */
104
+ agenticBrowsingCategoryTitle: 'Agentic Browsing',
105
+ /** Description of the Agentic Browsing category. */
106
+ agenticBrowsingCategoryDescription: 'These checks ensure high-quality, [browsable websites for AI agents](https://goo.gle/lighthouse-agentic-web) ' +
107
+ 'and validate the correctness of WebMCP integrations. ' +
108
+ 'This category is still under development and subject to change.',
109
+ /** Title of the WebMCP group of audits. */
110
+ webmcpGroupTitle: 'WebMCP',
111
+ /** Description of the WebMCP group. */
112
+ webmcpGroupDescription: 'Audits validating WebMCP integration.',
113
+ /** Title of the Agent Accessibility group of audits. */
114
+ agentAccessibilityGroupTitle: 'Agent Accessibility',
115
+ /** Description of the Agent Accessibility group of audits. */
116
+ agentAccessibilityGroupDescription: 'These audits highlight best practices for improving the ' +
117
+ 'accessibility of the website for AI agents.',
103
118
  };
104
119
 
105
120
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -108,6 +123,9 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
108
123
  const defaultConfig = {
109
124
  settings: constants.defaultSettings,
110
125
  artifacts: [
126
+ {id: 'WebMCP', gatherer: 'webmcp'},
127
+ {id: 'WebMcpSchemaIssues', gatherer: 'webmcp-schema'},
128
+ {id: 'LlmsTxt', gatherer: 'agentic/llms-txt'},
111
129
  // Artifacts which can be depended on come first.
112
130
  {id: 'DevtoolsLog', gatherer: 'devtools-log'},
113
131
  {id: 'Trace', gatherer: 'trace'},
@@ -254,6 +272,9 @@ const defaultConfig = {
254
272
  'accessibility/manual/offscreen-content-hidden',
255
273
  'accessibility/manual/use-landmarks',
256
274
  'accessibility/manual/visual-order-follows-dom',
275
+ 'accessibility/autocomplete-valid',
276
+ 'accessibility/presentation-role-conflict',
277
+ 'accessibility/svg-img-alt',
257
278
  'byte-efficiency/total-byte-weight',
258
279
  'byte-efficiency/unminified-css',
259
280
  'byte-efficiency/unminified-javascript',
@@ -276,6 +297,11 @@ const defaultConfig = {
276
297
  'seo/hreflang',
277
298
  'seo/canonical',
278
299
  'seo/manual/structured-data',
300
+ 'agentic/agent-accessibility-tree',
301
+ 'webmcp-registered-tools',
302
+ 'webmcp-form-coverage',
303
+ 'webmcp-schema-validity',
304
+ 'agentic/llms-txt',
279
305
  'bf-cache',
280
306
  'insights/cache-insight',
281
307
  'insights/cls-culprits-insight',
@@ -363,6 +389,14 @@ const defaultConfig = {
363
389
  'best-practices-general': {
364
390
  title: str_(UIStrings.bestPracticesGeneralGroupTitle),
365
391
  },
392
+ 'webmcp': {
393
+ title: str_(UIStrings.webmcpGroupTitle),
394
+ description: str_(UIStrings.webmcpGroupDescription),
395
+ },
396
+ 'agent-accessibility': {
397
+ title: str_(UIStrings.agentAccessibilityGroupTitle),
398
+ description: str_(UIStrings.agentAccessibilityGroupDescription),
399
+ },
366
400
  // Group for audits that should not be displayed.
367
401
  'hidden': {title: ''},
368
402
  },
@@ -515,6 +549,9 @@ const defaultConfig = {
515
549
  {id: 'valid-lang', weight: 7, group: 'a11y-language'}, // Serious, wcag2aa
516
550
  {id: 'video-caption', weight: 10, group: 'a11y-audio-video'}, // Critical, wcag2a
517
551
  {id: 'landmark-one-main', weight: 3, group: 'a11y-best-practices'}, // Moderate, best-practice
552
+ {id: 'autocomplete-valid', weight: 1, group: 'a11y-best-practices'}, // Informational
553
+ {id: 'presentation-role-conflict', weight: 1, group: 'a11y-best-practices'}, // Informational
554
+ {id: 'svg-img-alt', weight: 1, group: 'a11y-best-practices'}, // Informational
518
555
  // Manual audits
519
556
  {id: 'focusable-controls', weight: 0},
520
557
  {id: 'interactive-element-affordance', weight: 0},
@@ -595,6 +632,20 @@ const defaultConfig = {
595
632
  {id: 'structured-data', weight: 0},
596
633
  ],
597
634
  },
635
+ 'agentic-browsing': {
636
+ title: str_(UIStrings.agenticBrowsingCategoryTitle),
637
+ description: str_(UIStrings.agenticBrowsingCategoryDescription),
638
+ supportedModes: ['navigation', 'snapshot'],
639
+ categoryScoreDisplayMode: 'fraction',
640
+ auditRefs: [
641
+ {id: 'agent-accessibility-tree', weight: 1, group: 'agent-accessibility'},
642
+ {id: 'webmcp-form-coverage', weight: 1, group: 'webmcp'},
643
+ {id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'},
644
+ {id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'},
645
+ {id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'},
646
+ {id: 'llms-txt', weight: 1, group: 'agent-accessibility'},
647
+ ],
648
+ },
598
649
  },
599
650
  };
600
651
 
@@ -50,6 +50,7 @@ async function runA11yChecks() {
50
50
  'aria-roledescription': {enabled: false},
51
51
  'aria-treeitem-name': {enabled: true},
52
52
  'aria-text': {enabled: true},
53
+ 'autocomplete-valid': {enabled: true},
53
54
  'audio-caption': {enabled: false},
54
55
  'blink': {enabled: false},
55
56
  'duplicate-id': {enabled: false},
@@ -69,6 +70,7 @@ async function runA11yChecks() {
69
70
  // https://github.com/dequelabs/axe-core/issues/2958
70
71
  'nested-interactive': {enabled: false},
71
72
  'no-autoplay-audio': {enabled: false},
73
+ 'presentation-role-conflict': {enabled: true},
72
74
  'role-img-alt': {enabled: false},
73
75
  'scrollable-region-focusable': {enabled: false},
74
76
  'select-name': {enabled: true},
@@ -76,7 +78,7 @@ async function runA11yChecks() {
76
78
  'skip-link': {enabled: true},
77
79
  // https://github.com/GoogleChrome/lighthouse/issues/16163
78
80
  'summary-name': {enabled: false},
79
- 'svg-img-alt': {enabled: false},
81
+ 'svg-img-alt': {enabled: true},
80
82
  'tabindex': {enabled: true},
81
83
  'table-duplicate-name': {enabled: true},
82
84
  'table-fake-caption': {enabled: true},
@@ -176,6 +178,8 @@ function createAxeRuleResultArtifact(result) {
176
178
  tags: result.tags,
177
179
  nodes,
178
180
  error,
181
+ help: result.help,
182
+ description: result.description,
179
183
  };
180
184
  }
181
185
  /* c8 ignore stop */
@@ -0,0 +1,10 @@
1
+ export default LlmsTxt;
2
+ declare class LlmsTxt extends BaseGatherer {
3
+ /**
4
+ * @param {LH.Gatherer.Context} passContext
5
+ * @return {Promise<LH.Artifacts['LlmsTxt']>}
6
+ */
7
+ getArtifact(passContext: LH.Gatherer.Context): Promise<LH.Artifacts["LlmsTxt"]>;
8
+ }
9
+ import BaseGatherer from '../../base-gatherer.js';
10
+ //# sourceMappingURL=llms-txt.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+
8
+ import BaseGatherer from '../../base-gatherer.js';
9
+
10
+ class LlmsTxt extends BaseGatherer {
11
+ /** @type {LH.Gatherer.GathererMeta} */
12
+ meta = {
13
+ supportedModes: ['snapshot', 'navigation'],
14
+ };
15
+
16
+ /**
17
+ * @param {LH.Gatherer.Context} passContext
18
+ * @return {Promise<LH.Artifacts['LlmsTxt']>}
19
+ */
20
+ async getArtifact(passContext) {
21
+ const {finalDisplayedUrl} = passContext.baseArtifacts.URL;
22
+ const llmUrl = new URL('/llms.txt', finalDisplayedUrl).href;
23
+ return passContext.driver.fetcher.fetchResource(llmUrl)
24
+ .catch(err => ({status: null, content: null, errorMessage: err.message}));
25
+ }
26
+ }
27
+
28
+ export default LlmsTxt;
@@ -33,6 +33,8 @@ function collectElements() {
33
33
  autocomplete: formEl.autocomplete,
34
34
  // @ts-expect-error - getNodeDetails put into scope via stringification
35
35
  node: getNodeDetails(formEl),
36
+ webMcpToolname: formEl.getAttribute('toolname'),
37
+ webMcpTooldescription: formEl.getAttribute('tooldescription'),
36
38
  });
37
39
  }
38
40
 
@@ -32,7 +32,7 @@ function collectMetaElements() {
32
32
  property: getAttribute('property'),
33
33
  httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
34
34
  charset: getAttribute('charset'),
35
- node: functions.getNodeDetails(meta),
35
+ node: /** @type {LH.Artifacts.NodeDetails} */ (functions.getNodeDetails(meta)),
36
36
  };
37
37
  });
38
38
  }
@@ -142,7 +142,7 @@ class TraceElements extends BaseGatherer {
142
142
  }, new Set());
143
143
 
144
144
  // TODO: handle digging into Map in recursiveObjectEnumerate.
145
- for (const shift of insightSet.model.CLSCulprits.shifts.values()) {
145
+ for (const shift of insightSet.model.CLSCulprits?.shifts.values() ?? []) {
146
146
  nodeIds.push(...shift.unsizedImages.map(s => s.backendNodeId));
147
147
  }
148
148
 
@@ -0,0 +1,25 @@
1
+ export default WebMcpSchemaIssues;
2
+ declare class WebMcpSchemaIssues extends BaseGatherer {
3
+ /** @type {LH.Artifacts.WebMcpSchemaIssue[]} */
4
+ _issues: LH.Artifacts.WebMcpSchemaIssue[];
5
+ _onIssueAdded: (event: Record<string, any>) => void;
6
+ /**
7
+ * @param {Record<string, any>} event
8
+ */
9
+ onIssueAdded(event: Record<string, any>): void;
10
+ /**
11
+ * @param {LH.Gatherer.Context} passContext
12
+ */
13
+ startInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
14
+ /**
15
+ * @param {LH.Gatherer.Context} passContext
16
+ */
17
+ stopInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
18
+ /**
19
+ * @param {LH.Gatherer.Context} context
20
+ * @return {Promise<LH.Artifacts.WebMcpSchemaIssue[]>}
21
+ */
22
+ getArtifact(context: LH.Gatherer.Context): Promise<LH.Artifacts.WebMcpSchemaIssue[]>;
23
+ }
24
+ import BaseGatherer from '../base-gatherer.js';
25
+ //# sourceMappingURL=webmcp-schema.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import BaseGatherer from '../base-gatherer.js';
8
+ import {resolveNodeIdToObjectId} from '../driver/dom.js';
9
+ import {pageFunctions} from '../../lib/page-functions.js';
10
+ import {ExecutionContext} from '../driver/execution-context.js';
11
+
12
+ class WebMcpSchemaIssues extends BaseGatherer {
13
+ /** @type {LH.Gatherer.GathererMeta} */
14
+ meta = {
15
+ supportedModes: ['navigation', 'snapshot'],
16
+ };
17
+
18
+ constructor() {
19
+ super();
20
+ /** @type {LH.Artifacts.WebMcpSchemaIssue[]} */
21
+ this._issues = [];
22
+ this._onIssueAdded = this.onIssueAdded.bind(this);
23
+ }
24
+
25
+ /**
26
+ * @param {Record<string, any>} event
27
+ */
28
+ onIssueAdded(event) {
29
+ const issue = event.issue;
30
+ if (!issue || issue.code !== 'GenericIssue') return;
31
+
32
+ const details = issue.details?.genericIssueDetails;
33
+ if (!details) return;
34
+
35
+ const errorType = details.errorType;
36
+ if (errorType && (
37
+ errorType === 'FormModelContextMissingToolName' ||
38
+ errorType === 'FormModelContextMissingToolDescription' ||
39
+ errorType === 'FormModelContextRequiredParameterMissingName' ||
40
+ errorType === 'FormModelContextParameterMissingTitleAndDescription' ||
41
+ errorType === 'FormModelContextParameterMissingName'
42
+ )) {
43
+ this._issues.push(details);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {LH.Gatherer.Context} passContext
49
+ */
50
+ async startInstrumentation(passContext) {
51
+ const session = passContext.driver.defaultSession;
52
+ session.on('Audits.issueAdded', this._onIssueAdded);
53
+ await session.sendCommand('Audits.enable');
54
+ }
55
+
56
+ /**
57
+ * @param {LH.Gatherer.Context} passContext
58
+ */
59
+ async stopInstrumentation(passContext) {
60
+ const session = passContext.driver.defaultSession;
61
+ session.off('Audits.issueAdded', this._onIssueAdded);
62
+ }
63
+
64
+ /**
65
+ * @param {LH.Gatherer.Context} context
66
+ * @return {Promise<LH.Artifacts.WebMcpSchemaIssue[]>}
67
+ */
68
+ async getArtifact(context) {
69
+ const session = context.driver.defaultSession;
70
+
71
+ const deps = ExecutionContext.serializeDeps([
72
+ pageFunctions.getNodeDetails,
73
+ ]);
74
+
75
+ const promises = this._issues.map(async (issue) => {
76
+ const processedIssue = {...issue};
77
+ if (issue.violatingNodeId) {
78
+ try {
79
+ const objectId = await resolveNodeIdToObjectId(session, issue.violatingNodeId);
80
+ if (objectId) {
81
+ const response = await session.sendCommand('Runtime.callFunctionOn', {
82
+ objectId,
83
+ functionDeclaration: `function () {
84
+ ${deps}
85
+ return getNodeDetails(this);
86
+ }`,
87
+ returnByValue: true,
88
+ awaitPromise: true,
89
+ });
90
+ if (response && response.result && response.result.value) {
91
+ processedIssue.nodeDetails = response.result.value;
92
+ }
93
+ }
94
+ } catch (err) {
95
+ // Ignore error
96
+ }
97
+ }
98
+ return processedIssue;
99
+ });
100
+
101
+ return Promise.all(promises);
102
+ }
103
+ }
104
+
105
+ export default WebMcpSchemaIssues;
@@ -0,0 +1,58 @@
1
+ export default WebMCP;
2
+ export type WebMCPTool = {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: Record<string, any>;
6
+ frameId: string;
7
+ backendNodeId?: number | undefined;
8
+ stackTrace?: any;
9
+ nodeDetails?: import("../../index.js").Artifacts.NodeDetails | undefined;
10
+ };
11
+ /**
12
+ * @typedef {Object} WebMCPTool
13
+ * @property {string} name
14
+ * @property {string} description
15
+ * @property {Record<string, any>} inputSchema
16
+ * @property {string} frameId
17
+ * @property {number} [backendNodeId]
18
+ * @property {any} [stackTrace]
19
+ * @property {LH.Artifacts.NodeDetails} [nodeDetails]
20
+ */
21
+ declare class WebMCP extends BaseGatherer {
22
+ /** @type {WebMCPTool[]} */
23
+ _tools: WebMCPTool[];
24
+ _isSupported: boolean;
25
+ _onToolsAdded: (event: {
26
+ tools: WebMCPTool[];
27
+ }) => void;
28
+ _onToolsRemoved: (event: {
29
+ tools: WebMCPTool[];
30
+ }) => void;
31
+ /**
32
+ * @param {{tools: WebMCPTool[]}} event
33
+ */
34
+ onToolsAdded(event: {
35
+ tools: WebMCPTool[];
36
+ }): void;
37
+ /**
38
+ * @param {{tools: WebMCPTool[]}} event
39
+ */
40
+ onToolsRemoved(event: {
41
+ tools: WebMCPTool[];
42
+ }): void;
43
+ /**
44
+ * @param {LH.Gatherer.Context} passContext
45
+ */
46
+ startInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
47
+ /**
48
+ * @param {LH.Gatherer.Context} passContext
49
+ */
50
+ stopInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
51
+ /**
52
+ * @param {LH.Gatherer.Context} context
53
+ * @return {Promise<LH.Artifacts['WebMCP']>}
54
+ */
55
+ getArtifact(context: LH.Gatherer.Context): Promise<LH.Artifacts["WebMCP"]>;
56
+ }
57
+ import BaseGatherer from '../base-gatherer.js';
58
+ //# sourceMappingURL=webmcp.d.ts.map
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Capture WebMCP data
9
+ */
10
+
11
+ import BaseGatherer from '../base-gatherer.js';
12
+ import {resolveNodeIdToObjectId} from '../driver/dom.js';
13
+ import {pageFunctions} from '../../lib/page-functions.js';
14
+ import {ExecutionContext} from '../driver/execution-context.js';
15
+
16
+ /**
17
+ * @typedef {Object} WebMCPTool
18
+ * @property {string} name
19
+ * @property {string} description
20
+ * @property {Record<string, any>} inputSchema
21
+ * @property {string} frameId
22
+ * @property {number} [backendNodeId]
23
+ * @property {any} [stackTrace]
24
+ * @property {LH.Artifacts.NodeDetails} [nodeDetails]
25
+ */
26
+ class WebMCP extends BaseGatherer {
27
+ /** @type {LH.Gatherer.GathererMeta} */
28
+ meta = {
29
+ supportedModes: ['navigation', 'snapshot'],
30
+ };
31
+
32
+ constructor() {
33
+ super();
34
+ /** @type {WebMCPTool[]} */
35
+ this._tools = [];
36
+ this._isSupported = true;
37
+ this._onToolsAdded = this.onToolsAdded.bind(this);
38
+ this._onToolsRemoved = this.onToolsRemoved.bind(this);
39
+ }
40
+
41
+ /**
42
+ * @param {{tools: WebMCPTool[]}} event
43
+ */
44
+ // TODO: Handle WebMCP tools per frame.
45
+ onToolsAdded(event) {
46
+ // Note that as of M148, there is a bug in WebMCP CDP.
47
+ // While WebMCP is enabled, any newly registered tool will
48
+ // have an empty schema.
49
+ if (event.tools) {
50
+ this._tools.push(...event.tools);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {{tools: WebMCPTool[]}} event
56
+ */
57
+ onToolsRemoved(event) {
58
+ if (event.tools) {
59
+ const removedNames = new Set(event.tools.map(t => t.name));
60
+ this._tools = this._tools.filter(t => !removedNames.has(t.name));
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {LH.Gatherer.Context} passContext
66
+ */
67
+ async startInstrumentation(passContext) {
68
+ const session = passContext.driver.defaultSession;
69
+
70
+ // @ts-expect-error - WebMCP domain might not be in types yet.
71
+ session.on('WebMCP.toolsAdded', this._onToolsAdded);
72
+ // @ts-expect-error
73
+ session.on('WebMCP.toolsRemoved', this._onToolsRemoved);
74
+
75
+ try {
76
+ await session.sendCommand('WebMCP.enable');
77
+ } catch (err) {
78
+ if (err.message.includes('\'WebMCP.enable\' wasn\'t found')) {
79
+ this._isSupported = false;
80
+ return;
81
+ }
82
+ throw err;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * @param {LH.Gatherer.Context} passContext
88
+ */
89
+ async stopInstrumentation(passContext) {
90
+ const session = passContext.driver.defaultSession;
91
+ // @ts-expect-error
92
+ session.off('WebMCP.toolsAdded', this._onToolsAdded);
93
+ // @ts-expect-error
94
+ session.off('WebMCP.toolsRemoved', this._onToolsRemoved);
95
+ try {
96
+ await session.sendCommand('WebMCP.disable');
97
+ } catch (err) {
98
+ // Ignore errors
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {LH.Gatherer.Context} context
104
+ * @return {Promise<LH.Artifacts['WebMCP']>}
105
+ */
106
+ async getArtifact(context) {
107
+ const isSupported = await context.driver.executionContext.evaluate(
108
+ // @ts-expect-error - modelContext is not in types
109
+ () => typeof navigator.modelContext !== 'undefined',
110
+ {args: [], useIsolation: true}
111
+ );
112
+ if (!isSupported || !this._isSupported) {
113
+ return {isSupported: false, tools: []};
114
+ }
115
+
116
+ const session = context.driver.defaultSession;
117
+
118
+ // Remove duplicates based on name, keeping the latest occurrence.
119
+ const toolMap = new Map();
120
+ for (const tool of this._tools) {
121
+ toolMap.set(tool.name, tool);
122
+ }
123
+
124
+ const resolvedTools = [];
125
+ for (const tool of toolMap.values()) {
126
+ if (tool.backendNodeId) {
127
+ try {
128
+ const objectId = await resolveNodeIdToObjectId(session, tool.backendNodeId);
129
+ if (objectId) {
130
+ const deps = ExecutionContext.serializeDeps([
131
+ pageFunctions.getNodeDetails,
132
+ ]);
133
+ const response = await session.sendCommand('Runtime.callFunctionOn', {
134
+ objectId,
135
+ functionDeclaration: `function () {
136
+ ${deps}
137
+ return getNodeDetails(this);
138
+ }`,
139
+ returnByValue: true,
140
+ awaitPromise: true,
141
+ });
142
+ if (response && response.result && response.result.value) {
143
+ tool.nodeDetails = response.result.value;
144
+ }
145
+ }
146
+ } catch (err) {
147
+ // Ignore error
148
+ }
149
+ }
150
+ resolvedTools.push(tool);
151
+ }
152
+ return {
153
+ isSupported: true,
154
+ tools: resolvedTools,
155
+ };
156
+ }
157
+ }
158
+
159
+ export default WebMCP;
@@ -1,3 +1,3 @@
1
1
  {
2
- "date": "2026-03-18"
2
+ "date": "2026-05-01"
3
3
  }
@@ -381,7 +381,7 @@ class SourceMap {
381
381
  let nameIndex = 0;
382
382
  const names = map.names ?? [];
383
383
  const tokenIter = new TokenIterator(map.mappings);
384
- let sourceURL = this.#sourceInfos[sourceIndex].sourceURL;
384
+ let sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
385
385
  while (true) {
386
386
  if (tokenIter.peek() === ',') {
387
387
  tokenIter.next();
@@ -404,7 +404,7 @@ class SourceMap {
404
404
  const sourceIndexDelta = tokenIter.nextVLQ();
405
405
  if (sourceIndexDelta) {
406
406
  sourceIndex += sourceIndexDelta;
407
- sourceURL = this.#sourceInfos[sourceIndex].sourceURL;
407
+ sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
408
408
  }
409
409
  sourceLineNumber += tokenIter.nextVLQ();
410
410
  sourceColumnNumber += tokenIter.nextVLQ();