chrome-devtools-mcp 0.18.1 → 0.19.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 (40) hide show
  1. package/README.md +4 -3
  2. package/build/src/McpContext.js +215 -266
  3. package/build/src/McpPage.js +95 -0
  4. package/build/src/McpResponse.js +73 -29
  5. package/build/src/bin/chrome-devtools.js +184 -0
  6. package/build/src/bin/cliDefinitions.js +651 -0
  7. package/build/src/browser.js +5 -3
  8. package/build/src/cli.js +11 -1
  9. package/build/src/daemon/client.js +151 -0
  10. package/build/src/daemon/daemon.js +49 -15
  11. package/build/src/daemon/types.js +6 -0
  12. package/build/src/daemon/utils.js +56 -15
  13. package/build/src/main.js +5 -173
  14. package/build/src/server.js +209 -0
  15. package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
  16. package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
  17. package/build/src/third_party/bundled-packages.json +4 -3
  18. package/build/src/third_party/devtools-formatter-worker.js +5 -13
  19. package/build/src/third_party/index.js +1980 -396
  20. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
  21. package/build/src/tools/ToolDefinition.js +52 -0
  22. package/build/src/tools/console.js +3 -3
  23. package/build/src/tools/emulation.js +13 -45
  24. package/build/src/tools/extensions.js +17 -0
  25. package/build/src/tools/input.js +33 -33
  26. package/build/src/tools/lighthouse.js +123 -0
  27. package/build/src/tools/memory.js +5 -5
  28. package/build/src/tools/network.js +7 -7
  29. package/build/src/tools/pages.js +32 -32
  30. package/build/src/tools/performance.js +16 -14
  31. package/build/src/tools/screencast.js +5 -5
  32. package/build/src/tools/screenshot.js +6 -6
  33. package/build/src/tools/script.js +99 -49
  34. package/build/src/tools/slim/tools.js +18 -18
  35. package/build/src/tools/snapshot.js +5 -4
  36. package/build/src/tools/tools.js +2 -0
  37. package/build/src/types.js +6 -0
  38. package/build/src/utils/files.js +19 -0
  39. package/build/src/version.js +1 -1
  40. package/package.json +11 -8
@@ -6,8 +6,8 @@
6
6
  import { logger } from '../logger.js';
7
7
  import { zod } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
- import { CLOSE_PAGE_ERROR, defineTool, timeoutSchema } from './ToolDefinition.js';
10
- export const listPages = defineTool(args => {
9
+ import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './ToolDefinition.js';
10
+ export const listPages = definePageTool(args => {
11
11
  return {
12
12
  name: 'list_pages',
13
13
  description: `Get a list of pages ${args?.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`,
@@ -42,7 +42,7 @@ export const selectPage = defineTool({
42
42
  context.selectPage(page);
43
43
  response.setIncludePages(true);
44
44
  if (request.params.bringToFront) {
45
- await page.bringToFront();
45
+ await page.pptrPage.bringToFront();
46
46
  }
47
47
  },
48
48
  });
@@ -75,7 +75,7 @@ export const closePage = defineTool({
75
75
  });
76
76
  export const newPage = defineTool({
77
77
  name: 'new_page',
78
- description: `Creates a new page`,
78
+ description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
79
79
  annotations: {
80
80
  category: ToolCategory.NAVIGATION,
81
81
  readOnlyHint: false,
@@ -97,16 +97,16 @@ export const newPage = defineTool({
97
97
  handler: async (request, response, context) => {
98
98
  const page = await context.newPage(request.params.background, request.params.isolatedContext);
99
99
  await context.waitForEventsAfterAction(async () => {
100
- await page.goto(request.params.url, {
100
+ await page.pptrPage.goto(request.params.url, {
101
101
  timeout: request.params.timeout,
102
102
  });
103
103
  }, { timeout: request.params.timeout });
104
104
  response.setIncludePages(true);
105
105
  },
106
106
  });
107
- export const navigatePage = defineTool({
107
+ export const navigatePage = definePageTool({
108
108
  name: 'navigate_page',
109
- description: `Navigates the currently selected page to a URL.`,
109
+ description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
110
110
  annotations: {
111
111
  category: ToolCategory.NAVIGATION,
112
112
  readOnlyHint: false,
@@ -132,7 +132,7 @@ export const navigatePage = defineTool({
132
132
  ...timeoutSchema,
133
133
  },
134
134
  handler: async (request, response, context) => {
135
- const page = context.getSelectedPage();
135
+ const page = request.page;
136
136
  const options = {
137
137
  timeout: request.params.timeout,
138
138
  };
@@ -154,15 +154,15 @@ export const navigatePage = defineTool({
154
154
  void dialog.dismiss();
155
155
  }
156
156
  // We are not going to report the dialog like regular dialogs.
157
- context.clearDialog();
157
+ page.clearDialog();
158
158
  }
159
159
  };
160
160
  let initScriptId;
161
161
  if (request.params.initScript) {
162
- const { identifier } = await page.evaluateOnNewDocument(request.params.initScript);
162
+ const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
163
163
  initScriptId = identifier;
164
164
  }
165
- page.on('dialog', dialogHandler);
165
+ page.pptrPage.on('dialog', dialogHandler);
166
166
  try {
167
167
  await context.waitForEventsAfterAction(async () => {
168
168
  switch (request.params.type) {
@@ -171,7 +171,7 @@ export const navigatePage = defineTool({
171
171
  throw new Error('A URL is required for navigation of type=url.');
172
172
  }
173
173
  try {
174
- await page.goto(request.params.url, options);
174
+ await page.pptrPage.goto(request.params.url, options);
175
175
  response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
176
176
  }
177
177
  catch (error) {
@@ -180,8 +180,8 @@ export const navigatePage = defineTool({
180
180
  break;
181
181
  case 'back':
182
182
  try {
183
- await page.goBack(options);
184
- response.appendResponseLine(`Successfully navigated back to ${page.url()}.`);
183
+ await page.pptrPage.goBack(options);
184
+ response.appendResponseLine(`Successfully navigated back to ${page.pptrPage.url()}.`);
185
185
  }
186
186
  catch (error) {
187
187
  response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
@@ -189,8 +189,8 @@ export const navigatePage = defineTool({
189
189
  break;
190
190
  case 'forward':
191
191
  try {
192
- await page.goForward(options);
193
- response.appendResponseLine(`Successfully navigated forward to ${page.url()}.`);
192
+ await page.pptrPage.goForward(options);
193
+ response.appendResponseLine(`Successfully navigated forward to ${page.pptrPage.url()}.`);
194
194
  }
195
195
  catch (error) {
196
196
  response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
@@ -198,7 +198,7 @@ export const navigatePage = defineTool({
198
198
  break;
199
199
  case 'reload':
200
200
  try {
201
- await page.reload({
201
+ await page.pptrPage.reload({
202
202
  ...options,
203
203
  ignoreCache: request.params.ignoreCache,
204
204
  });
@@ -212,9 +212,9 @@ export const navigatePage = defineTool({
212
212
  }, { timeout: request.params.timeout });
213
213
  }
214
214
  finally {
215
- page.off('dialog', dialogHandler);
215
+ page.pptrPage.off('dialog', dialogHandler);
216
216
  if (initScriptId) {
217
- await page
217
+ await page.pptrPage
218
218
  .removeScriptToEvaluateOnNewDocument(initScriptId)
219
219
  .catch(error => {
220
220
  logger(`Failed to remove init script`, error);
@@ -224,7 +224,7 @@ export const navigatePage = defineTool({
224
224
  response.setIncludePages(true);
225
225
  },
226
226
  });
227
- export const resizePage = defineTool({
227
+ export const resizePage = definePageTool({
228
228
  name: 'resize_page',
229
229
  description: `Resizes the selected page's window so that the page has specified dimension`,
230
230
  annotations: {
@@ -235,11 +235,11 @@ export const resizePage = defineTool({
235
235
  width: zod.number().describe('Page width'),
236
236
  height: zod.number().describe('Page height'),
237
237
  },
238
- handler: async (request, response, context) => {
239
- const page = context.getSelectedPage();
238
+ handler: async (request, response, _context) => {
239
+ const page = request.page;
240
240
  try {
241
- const browser = page.browser();
242
- const windowId = await page.windowId();
241
+ const browser = page.pptrPage.browser();
242
+ const windowId = await page.pptrPage.windowId();
243
243
  const bounds = await browser.getWindowBounds(windowId);
244
244
  if (bounds.windowState === 'fullscreen') {
245
245
  // Have to call this twice on Ubuntu when the window is in fullscreen mode.
@@ -253,14 +253,14 @@ export const resizePage = defineTool({
253
253
  catch {
254
254
  // Window APIs are not supported on all platforms
255
255
  }
256
- await page.resize({
256
+ await page.pptrPage.resize({
257
257
  contentWidth: request.params.width,
258
258
  contentHeight: request.params.height,
259
259
  });
260
260
  response.setIncludePages(true);
261
261
  },
262
262
  });
263
- export const handleDialog = defineTool({
263
+ export const handleDialog = definePageTool({
264
264
  name: 'handle_dialog',
265
265
  description: `If a browser dialog was opened, use this command to handle it`,
266
266
  annotations: {
@@ -276,8 +276,9 @@ export const handleDialog = defineTool({
276
276
  .optional()
277
277
  .describe('Optional prompt text to enter into the dialog.'),
278
278
  },
279
- handler: async (request, response, context) => {
280
- const dialog = context.getDialog();
279
+ handler: async (request, response, _context) => {
280
+ const page = request.page;
281
+ const dialog = page.getDialog();
281
282
  if (!dialog) {
282
283
  throw new Error('No open dialog found');
283
284
  }
@@ -305,11 +306,11 @@ export const handleDialog = defineTool({
305
306
  break;
306
307
  }
307
308
  }
308
- context.clearDialog();
309
+ page.clearDialog();
309
310
  response.setIncludePages(true);
310
311
  },
311
312
  });
312
- export const getTabId = defineTool({
313
+ export const getTabId = definePageTool({
313
314
  name: 'get_tab_id',
314
315
  description: `Get the tab ID of the page`,
315
316
  annotations: {
@@ -324,8 +325,7 @@ export const getTabId = defineTool({
324
325
  },
325
326
  handler: async (request, response, context) => {
326
327
  const page = context.getPageById(request.params.pageId);
327
- // @ts-expect-error _tabId is internal.
328
- const tabId = page._tabId;
328
+ const tabId = page.pptrPage._tabId;
329
329
  response.setTabId(tabId);
330
330
  },
331
331
  });
@@ -8,14 +8,14 @@ import { logger } from '../logger.js';
8
8
  import { zod, DevTools } from '../third_party/index.js';
9
9
  import { parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
10
10
  import { ToolCategory } from './categories.js';
11
- import { defineTool } from './ToolDefinition.js';
11
+ import { definePageTool } from './ToolDefinition.js';
12
12
  const filePathSchema = zod
13
13
  .string()
14
14
  .optional()
15
15
  .describe('The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).');
16
- export const startTrace = defineTool({
16
+ export const startTrace = definePageTool({
17
17
  name: 'performance_start_trace',
18
- description: `Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.`,
18
+ description: `Start a performance trace on the selected webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.`,
19
19
  annotations: {
20
20
  category: ToolCategory.PERFORMANCE,
21
21
  readOnlyHint: false,
@@ -23,9 +23,11 @@ export const startTrace = defineTool({
23
23
  schema: {
24
24
  reload: zod
25
25
  .boolean()
26
+ .default(true)
26
27
  .describe('Determines if, once tracing has started, the current selected page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.'),
27
28
  autoStop: zod
28
29
  .boolean()
30
+ .default(true)
29
31
  .describe('Determines if the trace recording should be automatically stopped.'),
30
32
  filePath: filePathSchema,
31
33
  },
@@ -35,11 +37,11 @@ export const startTrace = defineTool({
35
37
  return;
36
38
  }
37
39
  context.setIsRunningPerformanceTrace(true);
38
- const page = context.getSelectedPage();
39
- const pageUrlForTracing = page.url();
40
+ const page = request.page;
41
+ const pageUrlForTracing = page.pptrPage.url();
40
42
  if (request.params.reload) {
41
43
  // Before starting the recording, navigate to about:blank to clear out any state.
42
- await page.goto('about:blank', {
44
+ await page.pptrPage.goto('about:blank', {
43
45
  waitUntil: ['networkidle0'],
44
46
  });
45
47
  }
@@ -64,26 +66,26 @@ export const startTrace = defineTool({
64
66
  'v8.execute',
65
67
  'v8',
66
68
  ];
67
- await page.tracing.start({
69
+ await page.pptrPage.tracing.start({
68
70
  categories,
69
71
  });
70
72
  if (request.params.reload) {
71
- await page.goto(pageUrlForTracing, {
73
+ await page.pptrPage.goto(pageUrlForTracing, {
72
74
  waitUntil: ['load'],
73
75
  });
74
76
  }
75
77
  if (request.params.autoStop) {
76
78
  await new Promise(resolve => setTimeout(resolve, 5_000));
77
- await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
79
+ await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
78
80
  }
79
81
  else {
80
82
  response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
81
83
  }
82
84
  },
83
85
  });
84
- export const stopTrace = defineTool({
86
+ export const stopTrace = definePageTool({
85
87
  name: 'performance_stop_trace',
86
- description: 'Stops the active performance trace recording on the selected page.',
88
+ description: 'Stop the active performance trace recording on the selected webpage.',
87
89
  annotations: {
88
90
  category: ToolCategory.PERFORMANCE,
89
91
  readOnlyHint: false,
@@ -95,11 +97,11 @@ export const stopTrace = defineTool({
95
97
  if (!context.isRunningPerformanceTrace()) {
96
98
  return;
97
99
  }
98
- const page = context.getSelectedPage();
99
- await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
100
+ const page = request.page;
101
+ await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
100
102
  },
101
103
  });
102
- export const analyzeInsight = defineTool({
104
+ export const analyzeInsight = definePageTool({
103
105
  name: 'performance_analyze_insight',
104
106
  description: 'Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.',
105
107
  annotations: {
@@ -8,12 +8,12 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { zod } from '../third_party/index.js';
10
10
  import { ToolCategory } from './categories.js';
11
- import { defineTool } from './ToolDefinition.js';
11
+ import { definePageTool } from './ToolDefinition.js';
12
12
  async function generateTempFilePath() {
13
13
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
14
14
  return path.join(dir, `screencast.mp4`);
15
15
  }
16
- export const startScreencast = defineTool({
16
+ export const startScreencast = definePageTool({
17
17
  name: 'screencast_start',
18
18
  description: 'Starts recording a screencast (video) of the selected page in mp4 format.',
19
19
  annotations: {
@@ -34,10 +34,10 @@ export const startScreencast = defineTool({
34
34
  }
35
35
  const filePath = request.params.path ?? (await generateTempFilePath());
36
36
  const resolvedPath = path.resolve(filePath);
37
- const page = context.getSelectedPage();
37
+ const page = request.page;
38
38
  let recorder;
39
39
  try {
40
- recorder = await page.screencast({
40
+ recorder = await page.pptrPage.screencast({
41
41
  path: resolvedPath,
42
42
  format: 'mp4',
43
43
  });
@@ -54,7 +54,7 @@ export const startScreencast = defineTool({
54
54
  response.appendResponseLine(`Screencast recording started. The recording will be saved to ${resolvedPath}. Use ${stopScreencast.name} to stop recording.`);
55
55
  },
56
56
  });
57
- export const stopScreencast = defineTool({
57
+ export const stopScreencast = definePageTool({
58
58
  name: 'screencast_stop',
59
59
  description: 'Stops the active screencast recording on the selected page.',
60
60
  annotations: {
@@ -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 screenshot = defineTool({
8
+ import { definePageTool } from './ToolDefinition.js';
9
+ export const screenshot = definePageTool({
10
10
  name: 'take_screenshot',
11
11
  description: `Take a screenshot of the page or element.`,
12
12
  annotations: {
@@ -44,10 +44,10 @@ export const screenshot = defineTool({
44
44
  }
45
45
  let pageOrHandle;
46
46
  if (request.params.uid) {
47
- pageOrHandle = await context.getElementByUid(request.params.uid);
47
+ pageOrHandle = await request.page.getElementByUid(request.params.uid);
48
48
  }
49
49
  else {
50
- pageOrHandle = context.getSelectedPage();
50
+ pageOrHandle = request.page.pptrPage;
51
51
  }
52
52
  const format = request.params.format;
53
53
  const quality = format === 'png' ? undefined : request.params.quality;
@@ -71,8 +71,8 @@ export const screenshot = defineTool({
71
71
  response.appendResponseLine(`Saved screenshot to ${file.filename}.`);
72
72
  }
73
73
  else if (screenshot.length >= 2_000_000) {
74
- const { filename } = await context.saveTemporaryFile(screenshot, `image/${request.params.format}`);
75
- response.appendResponseLine(`Saved screenshot to ${filename}.`);
74
+ const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot.${request.params.format}`);
75
+ response.appendResponseLine(`Saved screenshot to ${filepath}.`);
76
76
  }
77
77
  else {
78
78
  response.attachImage({
@@ -5,17 +5,18 @@
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 evaluateScript = defineTool({
10
- name: 'evaluate_script',
11
- description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
8
+ import { defineTool, pageIdSchema } from './ToolDefinition.js';
9
+ export const evaluateScript = defineTool(cliArgs => {
10
+ return {
11
+ name: 'evaluate_script',
12
+ description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
12
13
  so returned values have to be JSON-serializable.`,
13
- annotations: {
14
- category: ToolCategory.DEBUGGING,
15
- readOnlyHint: false,
16
- },
17
- schema: {
18
- function: zod.string().describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
14
+ annotations: {
15
+ category: ToolCategory.DEBUGGING,
16
+ readOnlyHint: false,
17
+ },
18
+ schema: {
19
+ function: zod.string().describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
19
20
  Example without arguments: \`() => {
20
21
  return document.title
21
22
  }\` or \`async () => {
@@ -25,47 +26,96 @@ Example with arguments: \`(el) => {
25
26
  return el.innerText;
26
27
  }\`
27
28
  `),
28
- args: zod
29
- .array(zod.object({
30
- uid: zod
29
+ args: zod
30
+ .array(zod
31
31
  .string()
32
- .describe('The uid of an element on the page from the page content snapshot'),
33
- }))
34
- .optional()
35
- .describe(`An optional list of arguments to pass to the function.`),
36
- },
37
- handler: async (request, response, context) => {
38
- const args = [];
39
- try {
40
- const frames = new Set();
41
- for (const el of request.params.args ?? []) {
42
- const handle = await context.getElementByUid(el.uid);
43
- frames.add(handle.frame);
44
- args.push(handle);
32
+ .describe('The uid of an element on the page from the page content snapshot'))
33
+ .optional()
34
+ .describe(`An optional list of arguments to pass to the function.`),
35
+ ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
36
+ ...(cliArgs?.categoryExtensions
37
+ ? {
38
+ serviceWorkerId: zod
39
+ .string()
40
+ .optional()
41
+ .describe(`An optional service worker id to evaluate the script in.`),
42
+ }
43
+ : {}),
44
+ },
45
+ handler: async (request, response, context) => {
46
+ const { serviceWorkerId, args: uidArgs, function: fnString, pageId, } = request.params;
47
+ if (cliArgs?.categoryExtensions && serviceWorkerId) {
48
+ if (uidArgs && uidArgs.length > 0) {
49
+ throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
50
+ }
51
+ if (pageId) {
52
+ throw new Error('specify either a pageId or a serviceWorkerId.');
53
+ }
54
+ const worker = await getWebWorker(context, serviceWorkerId);
55
+ await performEvaluation(worker, fnString, [], response, context);
56
+ return;
45
57
  }
46
- let pageOrFrame;
47
- // We can't evaluate the element handle across frames
48
- if (frames.size > 1) {
49
- throw new Error("Elements from different frames can't be evaluated together.");
58
+ const mcpPage = cliArgs?.experimentalPageIdRouting
59
+ ? context.getPageById(request.params.pageId)
60
+ : context.getSelectedMcpPage();
61
+ const page = mcpPage.pptrPage;
62
+ const args = [];
63
+ try {
64
+ const frames = new Set();
65
+ for (const uid of uidArgs ?? []) {
66
+ const handle = await mcpPage.getElementByUid(uid);
67
+ frames.add(handle.frame);
68
+ args.push(handle);
69
+ }
70
+ const evaluatable = await getPageOrFrame(page, frames);
71
+ await performEvaluation(evaluatable, fnString, args, response, context);
50
72
  }
51
- else {
52
- pageOrFrame = [...frames.values()][0] ?? context.getSelectedPage();
73
+ finally {
74
+ void Promise.allSettled(args.map(arg => arg.dispose()));
53
75
  }
54
- const fn = await pageOrFrame.evaluateHandle(`(${request.params.function})`);
55
- args.unshift(fn);
56
- await context.waitForEventsAfterAction(async () => {
57
- const result = await pageOrFrame.evaluate(async (fn, ...args) => {
58
- // @ts-expect-error no types.
59
- return JSON.stringify(await fn(...args));
60
- }, ...args);
61
- response.appendResponseLine('Script ran on page and returned:');
62
- response.appendResponseLine('```json');
63
- response.appendResponseLine(`${result}`);
64
- response.appendResponseLine('```');
65
- });
66
- }
67
- finally {
68
- void Promise.allSettled(args.map(arg => arg.dispose()));
69
- }
70
- },
76
+ },
77
+ };
71
78
  });
79
+ const performEvaluation = async (evaluatable, fnString, args, response, context) => {
80
+ const fn = await evaluatable.evaluateHandle(`(${fnString})`);
81
+ try {
82
+ await context.waitForEventsAfterAction(async () => {
83
+ const result = await evaluatable.evaluate(async (fn, ...args) => {
84
+ // @ts-expect-error no types for function fn
85
+ return JSON.stringify(await fn(...args));
86
+ }, fn, ...args);
87
+ response.appendResponseLine('Script ran on page and returned:');
88
+ response.appendResponseLine('```json');
89
+ response.appendResponseLine(`${result}`);
90
+ response.appendResponseLine('```');
91
+ });
92
+ }
93
+ finally {
94
+ void fn.dispose();
95
+ }
96
+ };
97
+ const getPageOrFrame = async (page, frames) => {
98
+ let pageOrFrame;
99
+ // We can't evaluate the element handle across frames
100
+ if (frames.size > 1) {
101
+ throw new Error("Elements from different frames can't be evaluated together.");
102
+ }
103
+ else {
104
+ pageOrFrame = [...frames.values()][0] ?? page;
105
+ }
106
+ return pageOrFrame;
107
+ };
108
+ const getWebWorker = async (context, serviceWorkerId) => {
109
+ const serviceWorkers = context.getExtensionServiceWorkers();
110
+ const serviceWorker = serviceWorkers.find((sw) => context.getExtensionServiceWorkerId(sw) === serviceWorkerId);
111
+ if (serviceWorker && serviceWorker.target) {
112
+ const worker = await serviceWorker.target.worker();
113
+ if (!worker) {
114
+ throw new Error('Service worker target not found.');
115
+ }
116
+ return worker;
117
+ }
118
+ else {
119
+ throw new Error('Service worker not found.');
120
+ }
121
+ };
@@ -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 screenshot = defineTool({
8
+ import { definePageTool } from '../ToolDefinition.js';
9
+ export const screenshot = definePageTool({
10
10
  name: 'screenshot',
11
11
  description: `Takes a screenshot`,
12
12
  annotations: {
@@ -16,16 +16,16 @@ export const screenshot = defineTool({
16
16
  },
17
17
  schema: {},
18
18
  handler: async (request, response, context) => {
19
- const page = context.getSelectedPage();
20
- const screenshot = await page.screenshot({
19
+ const page = request.page;
20
+ const screenshot = await page.pptrPage.screenshot({
21
21
  type: 'png',
22
22
  optimizeForSpeed: true,
23
23
  });
24
- const { filename } = await context.saveTemporaryFile(screenshot, `image/png`);
25
- response.appendResponseLine(filename);
24
+ const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot.png`);
25
+ response.appendResponseLine(filepath);
26
26
  },
27
27
  });
28
- export const navigate = defineTool({
28
+ export const navigate = definePageTool({
29
29
  name: 'navigate',
30
30
  description: `Loads a URL`,
31
31
  annotations: {
@@ -35,8 +35,8 @@ export const navigate = defineTool({
35
35
  schema: {
36
36
  url: zod.string().describe('URL to navigate to'),
37
37
  },
38
- handler: async (request, response, context) => {
39
- const page = context.getSelectedPage();
38
+ handler: async (request, response) => {
39
+ const page = request.page;
40
40
  const options = {
41
41
  timeout: 30_000,
42
42
  };
@@ -45,20 +45,20 @@ export const navigate = defineTool({
45
45
  response.appendResponseLine(`Accepted a beforeunload dialog.`);
46
46
  void dialog.accept();
47
47
  // We are not going to report the dialog like regular dialogs.
48
- context.clearDialog();
48
+ page.clearDialog();
49
49
  }
50
50
  };
51
- page.on('dialog', dialogHandler);
51
+ page.pptrPage.on('dialog', dialogHandler);
52
52
  try {
53
- await page.goto(request.params.url, options);
54
- response.appendResponseLine(`Navigated to ${page.url()}.`);
53
+ await page.pptrPage.goto(request.params.url, options);
54
+ response.appendResponseLine(`Navigated to ${page.pptrPage.url()}.`);
55
55
  }
56
56
  finally {
57
- page.off('dialog', dialogHandler);
57
+ page.pptrPage.off('dialog', dialogHandler);
58
58
  }
59
59
  },
60
60
  });
61
- export const evaluate = defineTool({
61
+ export const evaluate = definePageTool({
62
62
  name: 'evaluate',
63
63
  description: `Evaluates a JavaScript script`,
64
64
  annotations: {
@@ -68,10 +68,10 @@ export const evaluate = defineTool({
68
68
  schema: {
69
69
  script: zod.string().describe(`JS script to run on the page`),
70
70
  },
71
- handler: async (request, response, context) => {
72
- const page = context.getSelectedPage();
71
+ handler: async (request, response) => {
72
+ const page = request.page;
73
73
  try {
74
- const result = await page.evaluate(request.params.script);
74
+ const result = await page.pptrPage.evaluate(request.params.script);
75
75
  response.appendResponseLine(JSON.stringify(result));
76
76
  }
77
77
  catch (err) {
@@ -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, timeoutSchema } from './ToolDefinition.js';
9
- export const takeSnapshot = defineTool({
8
+ import { definePageTool, timeoutSchema } from './ToolDefinition.js';
9
+ export const takeSnapshot = definePageTool({
10
10
  name: 'take_snapshot',
11
11
  description: `Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique
12
12
  identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected
@@ -33,7 +33,7 @@ in the DevTools Elements panel (if any).`,
33
33
  });
34
34
  },
35
35
  });
36
- export const waitFor = defineTool({
36
+ export const waitFor = definePageTool({
37
37
  name: 'wait_for',
38
38
  description: `Wait for the specified text to appear on the selected page.`,
39
39
  annotations: {
@@ -48,7 +48,8 @@ export const waitFor = defineTool({
48
48
  ...timeoutSchema,
49
49
  },
50
50
  handler: async (request, response, context) => {
51
- await context.waitForTextOnPage(request.params.text, request.params.timeout);
51
+ const page = request.page;
52
+ await context.waitForTextOnPage(request.params.text, request.params.timeout, page.pptrPage);
52
53
  response.appendResponseLine(`Element matching one of ${JSON.stringify(request.params.text)} found.`);
53
54
  response.includeSnapshot();
54
55
  },
@@ -7,6 +7,7 @@ import * as consoleTools from './console.js';
7
7
  import * as emulationTools from './emulation.js';
8
8
  import * as extensionTools from './extensions.js';
9
9
  import * as inputTools from './input.js';
10
+ import * as lighthouseTools from './lighthouse.js';
10
11
  import * as memoryTools from './memory.js';
11
12
  import * as networkTools from './network.js';
12
13
  import * as pagesTools from './pages.js';
@@ -24,6 +25,7 @@ export const createTools = (args) => {
24
25
  ...Object.values(emulationTools),
25
26
  ...Object.values(extensionTools),
26
27
  ...Object.values(inputTools),
28
+ ...Object.values(lighthouseTools),
27
29
  ...Object.values(memoryTools),
28
30
  ...Object.values(networkTools),
29
31
  ...Object.values(pagesTools),