chrome-devtools-mcp 0.18.1 → 0.20.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 (44) hide show
  1. package/README.md +6 -5
  2. package/build/src/McpContext.js +242 -266
  3. package/build/src/McpPage.js +95 -0
  4. package/build/src/McpResponse.js +124 -48
  5. package/build/src/bin/chrome-devtools-cli-options.js +651 -0
  6. package/build/src/{cli.js → bin/chrome-devtools-mcp-cli-options.js} +12 -2
  7. package/build/src/bin/chrome-devtools-mcp-main.js +35 -0
  8. package/build/src/bin/chrome-devtools-mcp.js +21 -0
  9. package/build/src/bin/chrome-devtools.js +185 -0
  10. package/build/src/bin/cliDefinitions.js +615 -0
  11. package/build/src/browser.js +13 -12
  12. package/build/src/daemon/client.js +152 -0
  13. package/build/src/daemon/daemon.js +56 -17
  14. package/build/src/daemon/types.js +6 -0
  15. package/build/src/daemon/utils.js +57 -16
  16. package/build/src/index.js +204 -16
  17. package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
  18. package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
  19. package/build/src/third_party/bundled-packages.json +4 -3
  20. package/build/src/third_party/devtools-formatter-worker.js +5 -14
  21. package/build/src/third_party/index.js +2128 -472
  22. package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +7 -0
  23. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
  24. package/build/src/tools/ToolDefinition.js +52 -0
  25. package/build/src/tools/console.js +3 -3
  26. package/build/src/tools/emulation.js +13 -45
  27. package/build/src/tools/extensions.js +17 -0
  28. package/build/src/tools/input.js +33 -33
  29. package/build/src/tools/lighthouse.js +123 -0
  30. package/build/src/tools/memory.js +5 -5
  31. package/build/src/tools/network.js +7 -7
  32. package/build/src/tools/pages.js +32 -32
  33. package/build/src/tools/performance.js +16 -14
  34. package/build/src/tools/screencast.js +5 -5
  35. package/build/src/tools/screenshot.js +6 -6
  36. package/build/src/tools/script.js +99 -49
  37. package/build/src/tools/slim/tools.js +18 -18
  38. package/build/src/tools/snapshot.js +5 -4
  39. package/build/src/tools/tools.js +2 -0
  40. package/build/src/types.js +6 -0
  41. package/build/src/utils/files.js +19 -0
  42. package/build/src/version.js +1 -1
  43. package/package.json +15 -9
  44. package/build/src/main.js +0 -203
@@ -5,9 +5,33 @@
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
7
  export function defineTool(definition) {
8
+ if (typeof definition === 'function') {
9
+ const factory = definition;
10
+ return (args) => {
11
+ return factory(args);
12
+ };
13
+ }
8
14
  return definition;
9
15
  }
16
+ export function definePageTool(definition) {
17
+ if (typeof definition === 'function') {
18
+ return (args) => {
19
+ const tool = definition(args);
20
+ return {
21
+ ...tool,
22
+ pageScoped: true,
23
+ };
24
+ };
25
+ }
26
+ return {
27
+ ...definition,
28
+ pageScoped: true,
29
+ };
30
+ }
10
31
  export const CLOSE_PAGE_ERROR = 'The last open page cannot be closed. It is fine to keep it open.';
32
+ export const pageIdSchema = {
33
+ pageId: zod.number().optional().describe('Targets a specific page by ID.'),
34
+ };
11
35
  export const timeoutSchema = {
12
36
  timeout: zod
13
37
  .number()
@@ -18,3 +42,31 @@ export const timeoutSchema = {
18
42
  return value && value <= 0 ? undefined : value;
19
43
  }),
20
44
  };
45
+ export function viewportTransform(arg) {
46
+ if (!arg) {
47
+ return undefined;
48
+ }
49
+ const [dimensions, ...tags] = arg.split(',');
50
+ const isMobile = tags.includes('mobile');
51
+ const hasTouch = tags.includes('touch');
52
+ const isLandscape = tags.includes('landscape');
53
+ const [width, height, dpr] = dimensions.split('x').map(Number);
54
+ return {
55
+ width,
56
+ height,
57
+ deviceScaleFactor: dpr,
58
+ isMobile: isMobile,
59
+ isLandscape: isLandscape,
60
+ hasTouch: hasTouch,
61
+ };
62
+ }
63
+ export function geolocationTransform(arg) {
64
+ if (!arg) {
65
+ return undefined;
66
+ }
67
+ const [latitude, longitude] = arg.split('x').map(Number);
68
+ return {
69
+ latitude,
70
+ longitude,
71
+ };
72
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { ToolCategory } from './categories.js';
8
- import { defineTool } from './ToolDefinition.js';
8
+ import { definePageTool } from './ToolDefinition.js';
9
9
  const FILTERABLE_MESSAGE_TYPES = [
10
10
  'log',
11
11
  'debug',
@@ -28,7 +28,7 @@ const FILTERABLE_MESSAGE_TYPES = [
28
28
  'verbose',
29
29
  'issue',
30
30
  ];
31
- export const listConsoleMessages = defineTool({
31
+ export const listConsoleMessages = definePageTool({
32
32
  name: 'list_console_messages',
33
33
  description: 'List all console messages for the currently selected page since the last navigation.',
34
34
  annotations: {
@@ -67,7 +67,7 @@ export const listConsoleMessages = defineTool({
67
67
  });
68
68
  },
69
69
  });
70
- export const getConsoleMessage = defineTool({
70
+ export const getConsoleMessage = definePageTool({
71
71
  name: 'get_console_message',
72
72
  description: `Gets a console message by its ID. You can get all messages by calling ${listConsoleMessages.name}.`,
73
73
  annotations: {
@@ -6,13 +6,12 @@
6
6
  */
7
7
  import { zod, PredefinedNetworkConditions } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
- import { defineTool } from './ToolDefinition.js';
9
+ import { definePageTool, geolocationTransform, viewportTransform, } from './ToolDefinition.js';
10
10
  const throttlingOptions = [
11
- 'No emulation',
12
11
  'Offline',
13
12
  ...Object.keys(PredefinedNetworkConditions),
14
13
  ];
15
- export const emulate = defineTool({
14
+ export const emulate = definePageTool({
16
15
  name: 'emulate',
17
16
  description: `Emulates various features on the selected page.`,
18
17
  annotations: {
@@ -23,65 +22,34 @@ export const emulate = defineTool({
23
22
  networkConditions: zod
24
23
  .enum(throttlingOptions)
25
24
  .optional()
26
- .describe(`Throttle network. Set to "No emulation" to disable. If omitted, conditions remain unchanged.`),
25
+ .describe(`Throttle network. Omit to disable throttling.`),
27
26
  cpuThrottlingRate: zod
28
27
  .number()
29
28
  .min(1)
30
29
  .max(20)
31
30
  .optional()
32
- .describe('Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.'),
31
+ .describe('Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling'),
33
32
  geolocation: zod
34
- .object({
35
- latitude: zod
36
- .number()
37
- .min(-90)
38
- .max(90)
39
- .describe('Latitude between -90 and 90.'),
40
- longitude: zod
41
- .number()
42
- .min(-180)
43
- .max(180)
44
- .describe('Longitude between -180 and 180.'),
45
- })
46
- .nullable()
33
+ .string()
47
34
  .optional()
48
- .describe('Geolocation to emulate. Set to null to clear the geolocation override.'),
35
+ .transform(geolocationTransform)
36
+ .describe('Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit clear the geolocation override.'),
49
37
  userAgent: zod
50
38
  .string()
51
- .nullable()
52
39
  .optional()
53
- .describe('User agent to emulate. Set to null to clear the user agent override.'),
40
+ .describe('User agent to emulate. Set to empty string to clear the user agent override.'),
54
41
  colorScheme: zod
55
42
  .enum(['dark', 'light', 'auto'])
56
43
  .optional()
57
44
  .describe('Emulate the dark or the light mode. Set to "auto" to reset to the default.'),
58
45
  viewport: zod
59
- .object({
60
- width: zod.number().int().min(0).describe('Page width in pixels.'),
61
- height: zod.number().int().min(0).describe('Page height in pixels.'),
62
- deviceScaleFactor: zod
63
- .number()
64
- .min(0)
65
- .optional()
66
- .describe('Specify device scale factor (can be thought of as dpr).'),
67
- isMobile: zod
68
- .boolean()
69
- .optional()
70
- .describe('Whether the meta viewport tag is taken into account. Defaults to false.'),
71
- hasTouch: zod
72
- .boolean()
73
- .optional()
74
- .describe('Specifies if viewport supports touch events. This should be set to true for mobile devices.'),
75
- isLandscape: zod
76
- .boolean()
77
- .optional()
78
- .describe('Specifies if viewport is in landscape mode. Defaults to false.'),
79
- })
80
- .nullable()
46
+ .string()
81
47
  .optional()
82
- .describe('Viewport to emulate. Set to null to reset to the default viewport.'),
48
+ .transform(viewportTransform)
49
+ .describe(`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`),
83
50
  },
84
51
  handler: async (request, _response, context) => {
85
- await context.emulate(request.params);
52
+ const page = request.page;
53
+ await context.emulate(request.params, page.pptrPage);
86
54
  },
87
55
  });
@@ -77,3 +77,20 @@ export const reloadExtension = defineTool({
77
77
  response.appendResponseLine('Extension reloaded.');
78
78
  },
79
79
  });
80
+ export const triggerExtensionAction = defineTool({
81
+ name: 'trigger_extension_action',
82
+ description: 'Triggers an action in a Chrome extension.',
83
+ annotations: {
84
+ category: ToolCategory.EXTENSIONS,
85
+ readOnlyHint: false,
86
+ conditions: [EXTENSIONS_CONDITION],
87
+ },
88
+ schema: {
89
+ id: zod.string().describe('ID of the extension.'),
90
+ },
91
+ handler: async (request, response, context) => {
92
+ const { id } = request.params;
93
+ await context.triggerExtensionAction(id);
94
+ response.appendResponseLine(`Extension action triggered. Id: ${id}`);
95
+ },
96
+ });
@@ -7,7 +7,7 @@ import { logger } from '../logger.js';
7
7
  import { zod } from '../third_party/index.js';
8
8
  import { parseKey } from '../utils/keyboard.js';
9
9
  import { ToolCategory } from './categories.js';
10
- import { defineTool } from './ToolDefinition.js';
10
+ import { definePageTool } from './ToolDefinition.js';
11
11
  const dblClickSchema = zod
12
12
  .boolean()
13
13
  .optional()
@@ -26,7 +26,7 @@ function handleActionError(error, uid) {
26
26
  cause: error,
27
27
  });
28
28
  }
29
- export const click = defineTool({
29
+ export const click = definePageTool({
30
30
  name: 'click',
31
31
  description: `Clicks on the provided element`,
32
32
  annotations: {
@@ -42,7 +42,7 @@ export const click = defineTool({
42
42
  },
43
43
  handler: async (request, response, context) => {
44
44
  const uid = request.params.uid;
45
- const handle = await context.getElementByUid(uid);
45
+ const handle = await request.page.getElementByUid(uid);
46
46
  try {
47
47
  await context.waitForEventsAfterAction(async () => {
48
48
  await handle.asLocator().click({
@@ -64,7 +64,7 @@ export const click = defineTool({
64
64
  }
65
65
  },
66
66
  });
67
- export const clickAt = defineTool({
67
+ export const clickAt = definePageTool({
68
68
  name: 'click_at',
69
69
  description: `Clicks at the provided coordinates`,
70
70
  annotations: {
@@ -79,9 +79,9 @@ export const clickAt = defineTool({
79
79
  includeSnapshot: includeSnapshotSchema,
80
80
  },
81
81
  handler: async (request, response, context) => {
82
- const page = context.getSelectedPage();
82
+ const page = request.page;
83
83
  await context.waitForEventsAfterAction(async () => {
84
- await page.mouse.click(request.params.x, request.params.y, {
84
+ await page.pptrPage.mouse.click(request.params.x, request.params.y, {
85
85
  clickCount: request.params.dblClick ? 2 : 1,
86
86
  });
87
87
  });
@@ -93,7 +93,7 @@ export const clickAt = defineTool({
93
93
  }
94
94
  },
95
95
  });
96
- export const hover = defineTool({
96
+ export const hover = definePageTool({
97
97
  name: 'hover',
98
98
  description: `Hover over the provided element`,
99
99
  annotations: {
@@ -108,7 +108,7 @@ export const hover = defineTool({
108
108
  },
109
109
  handler: async (request, response, context) => {
110
110
  const uid = request.params.uid;
111
- const handle = await context.getElementByUid(uid);
111
+ const handle = await request.page.getElementByUid(uid);
112
112
  try {
113
113
  await context.waitForEventsAfterAction(async () => {
114
114
  await handle.asLocator().hover();
@@ -163,8 +163,8 @@ async function selectOption(handle, aXNode, value) {
163
163
  function hasOptionChildren(aXNode) {
164
164
  return aXNode.children.some(child => child.role === 'option');
165
165
  }
166
- async function fillFormElement(uid, value, context) {
167
- const handle = await context.getElementByUid(uid);
166
+ async function fillFormElement(uid, value, context, page) {
167
+ const handle = await page.getElementByUid(uid);
168
168
  try {
169
169
  const aXNode = context.getAXNodeByUid(uid);
170
170
  // We assume that combobox needs to be handled as select if it has
@@ -175,8 +175,7 @@ async function fillFormElement(uid, value, context) {
175
175
  else {
176
176
  // Increase timeout for longer input values.
177
177
  const timeoutPerChar = 10; // ms
178
- const fillTimeout = context.getSelectedPage().getDefaultTimeout() +
179
- value.length * timeoutPerChar;
178
+ const fillTimeout = page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
180
179
  await handle.asLocator().setTimeout(fillTimeout).fill(value);
181
180
  }
182
181
  }
@@ -187,7 +186,7 @@ async function fillFormElement(uid, value, context) {
187
186
  void handle.dispose();
188
187
  }
189
188
  }
190
- export const fill = defineTool({
189
+ export const fill = definePageTool({
191
190
  name: 'fill',
192
191
  description: `Type text into a input, text area or select an option from a <select> element.`,
193
192
  annotations: {
@@ -202,8 +201,9 @@ export const fill = defineTool({
202
201
  includeSnapshot: includeSnapshotSchema,
203
202
  },
204
203
  handler: async (request, response, context) => {
204
+ const page = request.page;
205
205
  await context.waitForEventsAfterAction(async () => {
206
- await fillFormElement(request.params.uid, request.params.value, context);
206
+ await fillFormElement(request.params.uid, request.params.value, context, page);
207
207
  });
208
208
  response.appendResponseLine(`Successfully filled out the element`);
209
209
  if (request.params.includeSnapshot) {
@@ -211,7 +211,7 @@ export const fill = defineTool({
211
211
  }
212
212
  },
213
213
  });
214
- export const typeText = defineTool({
214
+ export const typeText = definePageTool({
215
215
  name: 'type_text',
216
216
  description: `Type text using keyboard into a previously focused input`,
217
217
  annotations: {
@@ -223,17 +223,17 @@ export const typeText = defineTool({
223
223
  submitKey: submitKeySchema,
224
224
  },
225
225
  handler: async (request, response, context) => {
226
+ const page = request.page;
226
227
  await context.waitForEventsAfterAction(async () => {
227
- const page = context.getSelectedPage();
228
- await page.keyboard.type(request.params.text);
228
+ await page.pptrPage.keyboard.type(request.params.text);
229
229
  if (request.params.submitKey) {
230
- await page.keyboard.press(request.params.submitKey);
230
+ await page.pptrPage.keyboard.press(request.params.submitKey);
231
231
  }
232
232
  });
233
233
  response.appendResponseLine(`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`);
234
234
  },
235
235
  });
236
- export const drag = defineTool({
236
+ export const drag = definePageTool({
237
237
  name: 'drag',
238
238
  description: `Drag an element onto another element`,
239
239
  annotations: {
@@ -246,8 +246,8 @@ export const drag = defineTool({
246
246
  includeSnapshot: includeSnapshotSchema,
247
247
  },
248
248
  handler: async (request, response, context) => {
249
- const fromHandle = await context.getElementByUid(request.params.from_uid);
250
- const toHandle = await context.getElementByUid(request.params.to_uid);
249
+ const fromHandle = await request.page.getElementByUid(request.params.from_uid);
250
+ const toHandle = await request.page.getElementByUid(request.params.to_uid);
251
251
  try {
252
252
  await context.waitForEventsAfterAction(async () => {
253
253
  await fromHandle.drag(toHandle);
@@ -265,7 +265,7 @@ export const drag = defineTool({
265
265
  }
266
266
  },
267
267
  });
268
- export const fillForm = defineTool({
268
+ export const fillForm = definePageTool({
269
269
  name: 'fill_form',
270
270
  description: `Fill out multiple form elements at once`,
271
271
  annotations: {
@@ -282,9 +282,10 @@ export const fillForm = defineTool({
282
282
  includeSnapshot: includeSnapshotSchema,
283
283
  },
284
284
  handler: async (request, response, context) => {
285
+ const page = request.page;
285
286
  for (const element of request.params.elements) {
286
287
  await context.waitForEventsAfterAction(async () => {
287
- await fillFormElement(element.uid, element.value, context);
288
+ await fillFormElement(element.uid, element.value, context, page);
288
289
  });
289
290
  }
290
291
  response.appendResponseLine(`Successfully filled out the form`);
@@ -293,7 +294,7 @@ export const fillForm = defineTool({
293
294
  }
294
295
  },
295
296
  });
296
- export const uploadFile = defineTool({
297
+ export const uploadFile = definePageTool({
297
298
  name: 'upload_file',
298
299
  description: 'Upload a file through a provided element.',
299
300
  annotations: {
@@ -307,9 +308,9 @@ export const uploadFile = defineTool({
307
308
  filePath: zod.string().describe('The local path of the file to upload'),
308
309
  includeSnapshot: includeSnapshotSchema,
309
310
  },
310
- handler: async (request, response, context) => {
311
+ handler: async (request, response) => {
311
312
  const { uid, filePath } = request.params;
312
- const handle = (await context.getElementByUid(uid));
313
+ const handle = (await request.page.getElementByUid(uid));
313
314
  try {
314
315
  try {
315
316
  await handle.uploadFile(filePath);
@@ -319,9 +320,8 @@ export const uploadFile = defineTool({
319
320
  // a type=file element. In this case, we want to default to
320
321
  // Page.waitForFileChooser() and upload the file this way.
321
322
  try {
322
- const page = context.getSelectedPage();
323
323
  const [fileChooser] = await Promise.all([
324
- page.waitForFileChooser({ timeout: 3000 }),
324
+ request.page.pptrPage.waitForFileChooser({ timeout: 3000 }),
325
325
  handle.asLocator().click(),
326
326
  ]);
327
327
  await fileChooser.accept([filePath]);
@@ -340,7 +340,7 @@ export const uploadFile = defineTool({
340
340
  }
341
341
  },
342
342
  });
343
- export const pressKey = defineTool({
343
+ export const pressKey = definePageTool({
344
344
  name: 'press_key',
345
345
  description: `Press a key or key combination. Use this when other input methods like fill() cannot be used (e.g., keyboard shortcuts, navigation keys, or special key combinations).`,
346
346
  annotations: {
@@ -354,16 +354,16 @@ export const pressKey = defineTool({
354
354
  includeSnapshot: includeSnapshotSchema,
355
355
  },
356
356
  handler: async (request, response, context) => {
357
- const page = context.getSelectedPage();
357
+ const page = request.page;
358
358
  const tokens = parseKey(request.params.key);
359
359
  const [key, ...modifiers] = tokens;
360
360
  await context.waitForEventsAfterAction(async () => {
361
361
  for (const modifier of modifiers) {
362
- await page.keyboard.down(modifier);
362
+ await page.pptrPage.keyboard.down(modifier);
363
363
  }
364
- await page.keyboard.press(key);
364
+ await page.pptrPage.keyboard.press(key);
365
365
  for (const modifier of modifiers.toReversed()) {
366
- await page.keyboard.up(modifier);
366
+ await page.pptrPage.keyboard.up(modifier);
367
367
  }
368
368
  });
369
369
  response.appendResponseLine(`Successfully pressed key: ${request.params.key}`);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import path from 'node:path';
7
+ import { snapshot, navigation, generateReport, zod, } from '../third_party/index.js';
8
+ import { ToolCategory } from './categories.js';
9
+ import { startTrace } from './performance.js';
10
+ import { definePageTool } from './ToolDefinition.js';
11
+ export const lighthouseAudit = definePageTool({
12
+ name: 'lighthouse_audit',
13
+ description: `Get Lighthouse score and reports for accessibility, SEO and best practices. This excludes performance. For performance audits, run ${startTrace.name}`,
14
+ annotations: {
15
+ category: ToolCategory.DEBUGGING,
16
+ readOnlyHint: true,
17
+ },
18
+ schema: {
19
+ mode: zod
20
+ .enum(['navigation', 'snapshot'])
21
+ .default('navigation')
22
+ .describe('"navigation" reloads & audits. "snapshot" analyzes current state.'),
23
+ device: zod
24
+ .enum(['desktop', 'mobile'])
25
+ .default('desktop')
26
+ .describe('Device to emulate.'),
27
+ outputDirPath: zod
28
+ .string()
29
+ .optional()
30
+ .describe('Directory for reports. If omitted, uses temporary files.'),
31
+ },
32
+ handler: async (request, response, context) => {
33
+ const page = request.page;
34
+ const categories = ['accessibility', 'seo', 'best-practices'];
35
+ const formats = ['json', 'html'];
36
+ const { mode = 'navigation', device = 'desktop', outputDirPath, } = request.params;
37
+ const flags = {
38
+ onlyCategories: categories,
39
+ output: formats,
40
+ // Default 30 second timeout for page load.
41
+ maxWaitForLoad: 30_000,
42
+ };
43
+ if (device === 'desktop') {
44
+ flags.formFactor = 'desktop';
45
+ flags.screenEmulation = {
46
+ mobile: false,
47
+ width: 1350,
48
+ height: 940,
49
+ deviceScaleFactor: 1,
50
+ disabled: false,
51
+ };
52
+ }
53
+ else {
54
+ flags.formFactor = 'mobile';
55
+ flags.screenEmulation = {
56
+ mobile: true,
57
+ width: 412,
58
+ height: 823,
59
+ deviceScaleFactor: 1.75,
60
+ disabled: false,
61
+ };
62
+ }
63
+ let result;
64
+ try {
65
+ if (mode === 'navigation') {
66
+ result = await navigation(page.pptrPage, page.pptrPage.url(), {
67
+ flags,
68
+ });
69
+ }
70
+ else {
71
+ result = await snapshot(page.pptrPage, {
72
+ flags,
73
+ });
74
+ }
75
+ if (!result) {
76
+ throw new Error('Lighthouse audit failed.');
77
+ }
78
+ }
79
+ finally {
80
+ await context.restoreEmulation(page);
81
+ }
82
+ const lhr = result.lhr;
83
+ const reportPaths = [];
84
+ const encoder = new TextEncoder();
85
+ for (const format of formats) {
86
+ const report = generateReport(lhr, format);
87
+ const data = encoder.encode(report);
88
+ if (outputDirPath) {
89
+ const reportPath = path.join(outputDirPath, `report.${format}`);
90
+ const { filename } = await context.saveFile(data, reportPath);
91
+ reportPaths.push(filename);
92
+ }
93
+ else {
94
+ const { filepath } = await context.saveTemporaryFile(data, `report.${format}`);
95
+ reportPaths.push(filepath);
96
+ }
97
+ }
98
+ const categoryScores = Object.values(lhr.categories).map(c => ({
99
+ id: c.id,
100
+ title: c.title,
101
+ score: c.score,
102
+ }));
103
+ const failedAudits = Object.values(lhr.audits).filter(a => a.score !== null && a.score < 1).length;
104
+ const passedAudits = Object.values(lhr.audits).filter(a => a.score === 1).length;
105
+ const output = {
106
+ summary: {
107
+ mode,
108
+ device,
109
+ url: lhr.mainDocumentUrl,
110
+ scores: categoryScores,
111
+ audits: {
112
+ failed: failedAudits,
113
+ passed: passedAudits,
114
+ },
115
+ timing: {
116
+ total: lhr.timing.total,
117
+ },
118
+ },
119
+ reports: reportPaths,
120
+ };
121
+ response.attachLighthouseResult(output);
122
+ },
123
+ });
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { ToolCategory } from './categories.js';
8
- import { defineTool } from './ToolDefinition.js';
9
- export const takeMemorySnapshot = defineTool({
8
+ import { definePageTool } from './ToolDefinition.js';
9
+ export const takeMemorySnapshot = definePageTool({
10
10
  name: 'take_memory_snapshot',
11
11
  description: `Capture a memory heapsnapshot of the currently selected page to memory leak debugging`,
12
12
  annotations: {
@@ -18,9 +18,9 @@ export const takeMemorySnapshot = defineTool({
18
18
  .string()
19
19
  .describe('A path to a .heapsnapshot file to save the heapsnapshot to.'),
20
20
  },
21
- handler: async (request, response, context) => {
22
- const page = context.getSelectedPage();
23
- await page.captureHeapSnapshot({
21
+ handler: async (request, response, _context) => {
22
+ const page = request.page;
23
+ await page.pptrPage.captureHeapSnapshot({
24
24
  path: request.params.filePath,
25
25
  });
26
26
  response.appendResponseLine(`Heap snapshot saved to ${request.params.filePath}`);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { ToolCategory } from './categories.js';
8
- import { defineTool } from './ToolDefinition.js';
8
+ import { definePageTool } from './ToolDefinition.js';
9
9
  const FILTERABLE_RESOURCE_TYPES = [
10
10
  'document',
11
11
  'stylesheet',
@@ -27,7 +27,7 @@ const FILTERABLE_RESOURCE_TYPES = [
27
27
  'fedcm',
28
28
  'other',
29
29
  ];
30
- export const listNetworkRequests = defineTool({
30
+ export const listNetworkRequests = definePageTool({
31
31
  name: 'list_network_requests',
32
32
  description: `List all requests for the currently selected page since the last navigation.`,
33
33
  annotations: {
@@ -58,10 +58,10 @@ export const listNetworkRequests = defineTool({
58
58
  .describe('Set to true to return the preserved requests over the last 3 navigations.'),
59
59
  },
60
60
  handler: async (request, response, context) => {
61
- const data = await context.getDevToolsData();
61
+ const data = await context.getDevToolsData(request.page);
62
62
  response.attachDevToolsData(data);
63
63
  const reqid = data?.cdpRequestId
64
- ? context.resolveCdpRequestId(data.cdpRequestId)
64
+ ? context.resolveCdpRequestId(request.page, data.cdpRequestId)
65
65
  : undefined;
66
66
  response.setIncludeNetworkRequests(true, {
67
67
  pageSize: request.params.pageSize,
@@ -72,7 +72,7 @@ export const listNetworkRequests = defineTool({
72
72
  });
73
73
  },
74
74
  });
75
- export const getNetworkRequest = defineTool({
75
+ export const getNetworkRequest = definePageTool({
76
76
  name: 'get_network_request',
77
77
  description: `Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.`,
78
78
  annotations: {
@@ -101,10 +101,10 @@ export const getNetworkRequest = defineTool({
101
101
  });
102
102
  }
103
103
  else {
104
- const data = await context.getDevToolsData();
104
+ const data = await context.getDevToolsData(request.page);
105
105
  response.attachDevToolsData(data);
106
106
  const reqid = data?.cdpRequestId
107
- ? context.resolveCdpRequestId(data.cdpRequestId)
107
+ ? context.resolveCdpRequestId(request.page, data.cdpRequestId)
108
108
  : undefined;
109
109
  if (reqid) {
110
110
  response.attachNetworkRequest(reqid, {