chrome-devtools-mcp 0.21.0 → 0.23.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 (51) hide show
  1. package/README.md +87 -21
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -181
  4. package/build/src/McpPage.js +214 -0
  5. package/build/src/McpResponse.js +151 -13
  6. package/build/src/PageCollector.js +10 -24
  7. package/build/src/TextSnapshot.js +230 -0
  8. package/build/src/WaitForHelper.js +31 -0
  9. package/build/src/bin/check-latest-version.js +25 -0
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
  11. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  12. package/build/src/bin/chrome-devtools.js +25 -14
  13. package/build/src/bin/cliDefinitions.js +14 -8
  14. package/build/src/daemon/client.js +11 -11
  15. package/build/src/daemon/daemon.js +6 -9
  16. package/build/src/daemon/utils.js +19 -14
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +12 -1
  20. package/build/src/telemetry/ClearcutLogger.js +34 -12
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
  25. package/build/src/third_party/bundled-packages.json +6 -4
  26. package/build/src/third_party/devtools-formatter-worker.js +61 -64
  27. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  28. package/build/src/third_party/index.js +62661 -60590
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
  30. package/build/src/tools/categories.js +3 -0
  31. package/build/src/tools/console.js +42 -39
  32. package/build/src/tools/emulation.js +1 -1
  33. package/build/src/tools/extensions.js +5 -11
  34. package/build/src/tools/inPage.js +3 -13
  35. package/build/src/tools/input.js +15 -16
  36. package/build/src/tools/lighthouse.js +2 -2
  37. package/build/src/tools/memory.js +48 -3
  38. package/build/src/tools/network.js +4 -4
  39. package/build/src/tools/pages.js +212 -146
  40. package/build/src/tools/performance.js +1 -1
  41. package/build/src/tools/screencast.js +20 -8
  42. package/build/src/tools/screenshot.js +3 -3
  43. package/build/src/tools/script.js +22 -16
  44. package/build/src/tools/tools.js +2 -0
  45. package/build/src/tools/webmcp.js +63 -0
  46. package/build/src/utils/check-for-updates.js +73 -0
  47. package/build/src/utils/files.js +4 -0
  48. package/build/src/utils/id.js +15 -0
  49. package/build/src/version.js +1 -1
  50. package/package.json +13 -8
  51. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -7,10 +7,55 @@ import { logger } from '../logger.js';
7
7
  import { zod } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './ToolDefinition.js';
10
+ async function navigateWithInterception(page, action, allowListString, timeout) {
11
+ const allowList = allowListString
12
+ ? allowListString.split(',').map((p) => new URLPattern(p.trim()))
13
+ : undefined;
14
+ const requestHandler = (interceptedRequest) => {
15
+ if (!interceptedRequest.isNavigationRequest()) {
16
+ void interceptedRequest.continue();
17
+ return;
18
+ }
19
+ const requestUrl = interceptedRequest.url();
20
+ const isAllowed = allowList.some((pattern) => pattern.test(requestUrl));
21
+ if (isAllowed) {
22
+ void interceptedRequest.continue();
23
+ }
24
+ else {
25
+ logger(`Blocking request to: ${requestUrl}`);
26
+ void interceptedRequest.abort('blockedbyclient');
27
+ }
28
+ };
29
+ const cleanupInterception = async () => {
30
+ if (allowList) {
31
+ page.pptrPage.off('request', requestHandler);
32
+ await page.pptrPage.setRequestInterception(false).catch(error => {
33
+ logger(`Failed to disable request interception`, error);
34
+ });
35
+ }
36
+ };
37
+ if (allowList) {
38
+ await page.pptrPage.setRequestInterception(true);
39
+ page.pptrPage.on('request', requestHandler);
40
+ }
41
+ try {
42
+ await page.waitForEventsAfterAction(async () => {
43
+ try {
44
+ await action();
45
+ }
46
+ finally {
47
+ await cleanupInterception();
48
+ }
49
+ }, { timeout });
50
+ }
51
+ finally {
52
+ await cleanupInterception();
53
+ }
54
+ }
10
55
  export const listPages = defineTool(args => {
11
56
  return {
12
57
  name: 'list_pages',
13
- description: `Get a list of pages ${args?.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`,
58
+ description: `Get a list of pages${args?.categoryExtensions ? ' including extension service workers' : ''} open in the browser.`,
14
59
  annotations: {
15
60
  category: ToolCategory.NAVIGATION,
16
61
  readOnlyHint: true,
@@ -19,6 +64,7 @@ export const listPages = defineTool(args => {
19
64
  handler: async (_request, response) => {
20
65
  response.setIncludePages(true);
21
66
  response.setListInPageTools();
67
+ response.setListWebMcpTools();
22
68
  },
23
69
  };
24
70
  });
@@ -43,6 +89,7 @@ export const selectPage = defineTool({
43
89
  context.selectPage(page);
44
90
  response.setIncludePages(true);
45
91
  response.setListInPageTools();
92
+ response.setListWebMcpTools();
46
93
  if (request.params.bringToFront) {
47
94
  await page.pptrPage.bringToFront();
48
95
  }
@@ -76,158 +123,177 @@ export const closePage = defineTool({
76
123
  response.setListInPageTools();
77
124
  },
78
125
  });
79
- export const newPage = defineTool({
80
- name: 'new_page',
81
- description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
82
- annotations: {
83
- category: ToolCategory.NAVIGATION,
84
- readOnlyHint: false,
85
- },
86
- schema: {
87
- url: zod.string().describe('URL to load in a new page.'),
88
- background: zod
89
- .boolean()
90
- .optional()
91
- .describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
92
- isolatedContext: zod
93
- .string()
94
- .optional()
95
- .describe('If specified, the page is created in an isolated browser context with the given name. ' +
96
- 'Pages in the same browser context share cookies and storage. ' +
97
- 'Pages in different browser contexts are fully isolated.'),
98
- ...timeoutSchema,
99
- },
100
- handler: async (request, response, context) => {
101
- const page = await context.newPage(request.params.background, request.params.isolatedContext);
102
- await context.waitForEventsAfterAction(async () => {
103
- await page.pptrPage.goto(request.params.url, {
126
+ export const newPage = defineTool(args => {
127
+ return {
128
+ name: 'new_page',
129
+ description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
130
+ annotations: {
131
+ category: ToolCategory.NAVIGATION,
132
+ readOnlyHint: false,
133
+ },
134
+ schema: {
135
+ url: zod.string().describe('URL to load in a new page.'),
136
+ background: zod
137
+ .boolean()
138
+ .optional()
139
+ .describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
140
+ isolatedContext: zod
141
+ .string()
142
+ .optional()
143
+ .describe('If specified, the page is created in an isolated browser context with the given name. ' +
144
+ 'Pages in the same browser context share cookies and storage. ' +
145
+ 'Pages in different browser contexts are fully isolated.'),
146
+ ...(args?.experimentalNavigationAllowlist
147
+ ? {
148
+ allowList: zod
149
+ .string()
150
+ .optional()
151
+ .describe('Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.'),
152
+ }
153
+ : {}),
154
+ ...timeoutSchema,
155
+ },
156
+ handler: async (request, response, context) => {
157
+ const page = await context.newPage(request.params.background, request.params.isolatedContext);
158
+ await navigateWithInterception(page, () => page.pptrPage.goto(request.params.url, {
104
159
  timeout: request.params.timeout,
105
- });
106
- }, { timeout: request.params.timeout });
107
- response.setIncludePages(true);
108
- response.setListInPageTools();
109
- },
160
+ }), request.params.allowList, request.params.timeout);
161
+ response.setIncludePages(true);
162
+ response.setListInPageTools();
163
+ },
164
+ };
110
165
  });
111
- export const navigatePage = definePageTool({
112
- name: 'navigate_page',
113
- description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
114
- annotations: {
115
- category: ToolCategory.NAVIGATION,
116
- readOnlyHint: false,
117
- },
118
- schema: {
119
- type: zod
120
- .enum(['url', 'back', 'forward', 'reload'])
121
- .optional()
122
- .describe('Navigate the page by URL, back or forward in history, or reload.'),
123
- url: zod.string().optional().describe('Target URL (only type=url)'),
124
- ignoreCache: zod
125
- .boolean()
126
- .optional()
127
- .describe('Whether to ignore cache on reload.'),
128
- handleBeforeUnload: zod
129
- .enum(['accept', 'decline'])
130
- .optional()
131
- .describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
132
- initScript: zod
133
- .string()
134
- .optional()
135
- .describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
136
- ...timeoutSchema,
137
- },
138
- handler: async (request, response, context) => {
139
- const page = request.page;
140
- const options = {
141
- timeout: request.params.timeout,
142
- };
143
- if (!request.params.type && !request.params.url) {
144
- throw new Error('Either URL or a type is required.');
145
- }
146
- if (!request.params.type) {
147
- request.params.type = 'url';
148
- }
149
- const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
150
- const dialogHandler = (dialog) => {
151
- if (dialog.type() === 'beforeunload') {
152
- if (handleBeforeUnload === 'accept') {
153
- response.appendResponseLine(`Accepted a beforeunload dialog.`);
154
- void dialog.accept();
166
+ export const navigatePage = definePageTool(args => {
167
+ return {
168
+ name: 'navigate_page',
169
+ description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
170
+ annotations: {
171
+ category: ToolCategory.NAVIGATION,
172
+ readOnlyHint: false,
173
+ },
174
+ schema: {
175
+ type: zod
176
+ .enum(['url', 'back', 'forward', 'reload'])
177
+ .optional()
178
+ .describe('Navigate the page by URL, back or forward in history, or reload.'),
179
+ url: zod.string().optional().describe('Target URL (only type=url)'),
180
+ ignoreCache: zod
181
+ .boolean()
182
+ .optional()
183
+ .describe('Whether to ignore cache on reload.'),
184
+ handleBeforeUnload: zod
185
+ .enum(['accept', 'decline'])
186
+ .optional()
187
+ .describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
188
+ initScript: zod
189
+ .string()
190
+ .optional()
191
+ .describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
192
+ ...(args?.experimentalNavigationAllowlist
193
+ ? {
194
+ allowList: zod
195
+ .string()
196
+ .optional()
197
+ .describe('Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.'),
155
198
  }
156
- else {
157
- response.appendResponseLine(`Declined a beforeunload dialog.`);
158
- void dialog.dismiss();
199
+ : {}),
200
+ ...timeoutSchema,
201
+ },
202
+ handler: async (request, response) => {
203
+ const page = request.page;
204
+ const options = {
205
+ timeout: request.params.timeout,
206
+ };
207
+ if (!request.params.type && !request.params.url) {
208
+ throw new Error('Either URL or a type is required.');
209
+ }
210
+ if (!request.params.type) {
211
+ request.params.type = 'url';
212
+ }
213
+ const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
214
+ const dialogHandler = (dialog) => {
215
+ if (dialog.type() === 'beforeunload') {
216
+ if (handleBeforeUnload === 'accept') {
217
+ response.appendResponseLine(`Accepted a beforeunload dialog.`);
218
+ void dialog.accept();
219
+ }
220
+ else {
221
+ response.appendResponseLine(`Declined a beforeunload dialog.`);
222
+ void dialog.dismiss();
223
+ }
224
+ // We are not going to report the dialog like regular dialogs.
225
+ page.clearDialog();
159
226
  }
160
- // We are not going to report the dialog like regular dialogs.
161
- page.clearDialog();
227
+ };
228
+ let initScriptId;
229
+ if (request.params.initScript) {
230
+ const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
231
+ initScriptId = identifier;
162
232
  }
163
- };
164
- let initScriptId;
165
- if (request.params.initScript) {
166
- const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
167
- initScriptId = identifier;
168
- }
169
- page.pptrPage.on('dialog', dialogHandler);
170
- try {
171
- await context.waitForEventsAfterAction(async () => {
172
- switch (request.params.type) {
173
- case 'url':
174
- if (!request.params.url) {
175
- throw new Error('A URL is required for navigation of type=url.');
176
- }
177
- try {
178
- await page.pptrPage.goto(request.params.url, options);
179
- response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
180
- }
181
- catch (error) {
182
- response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
183
- }
184
- break;
185
- case 'back':
186
- try {
187
- await page.pptrPage.goBack(options);
188
- response.appendResponseLine(`Successfully navigated back to ${page.pptrPage.url()}.`);
189
- }
190
- catch (error) {
191
- response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
192
- }
193
- break;
194
- case 'forward':
195
- try {
196
- await page.pptrPage.goForward(options);
197
- response.appendResponseLine(`Successfully navigated forward to ${page.pptrPage.url()}.`);
198
- }
199
- catch (error) {
200
- response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
201
- }
202
- break;
203
- case 'reload':
204
- try {
205
- await page.pptrPage.reload({
206
- ...options,
207
- ignoreCache: request.params.ignoreCache,
208
- });
209
- response.appendResponseLine(`Successfully reloaded the page.`);
210
- }
211
- catch (error) {
212
- response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
213
- }
214
- break;
233
+ page.pptrPage.on('dialog', dialogHandler);
234
+ try {
235
+ await navigateWithInterception(page, async () => {
236
+ switch (request.params.type) {
237
+ case 'url':
238
+ if (!request.params.url) {
239
+ throw new Error('A URL is required for navigation of type=url.');
240
+ }
241
+ try {
242
+ await page.pptrPage.goto(request.params.url, options);
243
+ response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
244
+ }
245
+ catch (error) {
246
+ response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
247
+ }
248
+ break;
249
+ case 'back':
250
+ try {
251
+ await page.pptrPage.goBack(options);
252
+ response.appendResponseLine(`Successfully navigated back to ${page.pptrPage.url()}.`);
253
+ }
254
+ catch (error) {
255
+ response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
256
+ }
257
+ break;
258
+ case 'forward':
259
+ try {
260
+ await page.pptrPage.goForward(options);
261
+ response.appendResponseLine(`Successfully navigated forward to ${page.pptrPage.url()}.`);
262
+ }
263
+ catch (error) {
264
+ response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
265
+ }
266
+ break;
267
+ case 'reload':
268
+ try {
269
+ await page.pptrPage.reload({
270
+ ...options,
271
+ ignoreCache: request.params.ignoreCache,
272
+ });
273
+ response.appendResponseLine(`Successfully reloaded the page.`);
274
+ }
275
+ catch (error) {
276
+ response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
277
+ }
278
+ break;
279
+ }
280
+ }, request.params.allowList, request.params.timeout);
281
+ }
282
+ finally {
283
+ page.pptrPage.off('dialog', dialogHandler);
284
+ if (initScriptId) {
285
+ await page.pptrPage
286
+ .removeScriptToEvaluateOnNewDocument(initScriptId)
287
+ .catch(error => {
288
+ logger(`Failed to remove init script`, error);
289
+ });
215
290
  }
216
- }, { timeout: request.params.timeout });
217
- }
218
- finally {
219
- page.pptrPage.off('dialog', dialogHandler);
220
- if (initScriptId) {
221
- await page.pptrPage
222
- .removeScriptToEvaluateOnNewDocument(initScriptId)
223
- .catch(error => {
224
- logger(`Failed to remove init script`, error);
225
- });
226
291
  }
227
- }
228
- response.setIncludePages(true);
229
- response.setListInPageTools();
230
- },
292
+ response.setIncludePages(true);
293
+ response.setListInPageTools();
294
+ response.setListWebMcpTools();
295
+ },
296
+ };
231
297
  });
232
298
  export const resizePage = definePageTool({
233
299
  name: 'resize_page',
@@ -142,7 +142,7 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
142
142
  });
143
143
  });
144
144
  }
145
- const file = await context.saveFile(dataToWrite, filePath);
145
+ const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.json.gz' : '.json');
146
146
  response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
147
147
  }
148
148
  const result = await parseRawTraceBuffer(traceEventsBuffer);
@@ -7,39 +7,51 @@ import fs from 'node:fs/promises';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { zod } from '../third_party/index.js';
10
+ import { ensureExtension } from '../utils/files.js';
10
11
  import { ToolCategory } from './categories.js';
11
12
  import { definePageTool } from './ToolDefinition.js';
12
13
  async function generateTempFilePath() {
13
14
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
14
15
  return path.join(dir, `screencast.mp4`);
15
16
  }
16
- export const startScreencast = definePageTool({
17
+ const supportedExtensions = ['.webm', '.mp4'];
18
+ export const startScreencast = definePageTool(args => ({
17
19
  name: 'screencast_start',
18
- description: 'Starts recording a screencast (video) of the selected page in mp4 format.',
20
+ description: `Starts recording a screencast (video) of the selected page in specified format.`,
19
21
  annotations: {
20
22
  category: ToolCategory.DEBUGGING,
21
23
  readOnlyHint: false,
22
24
  conditions: ['screencast'],
23
25
  },
24
26
  schema: {
25
- path: zod
27
+ filePath: zod
26
28
  .string()
27
29
  .optional()
28
- .describe('Output path. Uses mkdtemp to generate a unique path if not provided.'),
30
+ .describe(`Output file path (${supportedExtensions.join(',')} are supported). Uses mkdtemp to generate a unique path if not provided.`),
29
31
  },
30
32
  handler: async (request, response, context) => {
31
33
  if (context.getScreenRecorder() !== null) {
32
34
  response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
33
35
  return;
34
36
  }
35
- const filePath = request.params.path ?? (await generateTempFilePath());
36
- const resolvedPath = path.resolve(filePath);
37
+ const filePath = request.params.filePath ?? (await generateTempFilePath());
38
+ let enforcedExtension = '.mp4';
39
+ let format = 'mp4';
40
+ for (const supportedExtension of supportedExtensions) {
41
+ if (filePath.endsWith(supportedExtension)) {
42
+ enforcedExtension = supportedExtension;
43
+ format = supportedExtension.substring(1);
44
+ break;
45
+ }
46
+ }
47
+ const resolvedPath = ensureExtension(path.resolve(filePath), enforcedExtension);
37
48
  const page = request.page;
38
49
  let recorder;
39
50
  try {
40
51
  recorder = await page.pptrPage.screencast({
41
52
  path: resolvedPath,
42
- format: 'mp4',
53
+ format: format,
54
+ ffmpegPath: args?.experimentalFfmpegPath,
43
55
  });
44
56
  }
45
57
  catch (err) {
@@ -53,7 +65,7 @@ export const startScreencast = definePageTool({
53
65
  context.setScreenRecorder({ recorder, filePath: resolvedPath });
54
66
  response.appendResponseLine(`Screencast recording started. The recording will be saved to ${resolvedPath}. Use ${stopScreencast.name} to stop recording.`);
55
67
  },
56
- });
68
+ }));
57
69
  export const stopScreencast = definePageTool({
58
70
  name: 'screencast_stop',
59
71
  description: 'Stops the active screencast recording on the selected page.',
@@ -28,7 +28,7 @@ export const screenshot = definePageTool({
28
28
  uid: zod
29
29
  .string()
30
30
  .optional()
31
- .describe('The uid of an element on the page from the page content snapshot. If omitted takes a pages screenshot.'),
31
+ .describe('The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.'),
32
32
  fullPage: zod
33
33
  .boolean()
34
34
  .optional()
@@ -67,8 +67,8 @@ export const screenshot = definePageTool({
67
67
  response.appendResponseLine("Took a screenshot of the current page's viewport.");
68
68
  }
69
69
  if (request.params.filePath) {
70
- const file = await context.saveFile(screenshot, request.params.filePath);
71
- response.appendResponseLine(`Saved screenshot to ${file.filename}.`);
70
+ const result = await context.saveFile(screenshot, request.params.filePath, `.${format}`);
71
+ response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
72
72
  }
73
73
  else if (screenshot.length >= 2_000_000) {
74
74
  const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot.${request.params.format}`);
@@ -9,7 +9,7 @@ import { defineTool, pageIdSchema } from './ToolDefinition.js';
9
9
  export const evaluateScript = defineTool(cliArgs => {
10
10
  return {
11
11
  name: 'evaluate_script',
12
- description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
12
+ description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON,
13
13
  so returned values have to be JSON-serializable.`,
14
14
  annotations: {
15
15
  category: ToolCategory.DEBUGGING,
@@ -32,18 +32,22 @@ Example with arguments: \`(el) => {
32
32
  .describe('The uid of an element on the page from the page content snapshot'))
33
33
  .optional()
34
34
  .describe(`An optional list of arguments to pass to the function.`),
35
+ dialogAction: zod
36
+ .string()
37
+ .optional()
38
+ .describe('Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.'),
35
39
  ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
36
40
  ...(cliArgs?.categoryExtensions
37
41
  ? {
38
42
  serviceWorkerId: zod
39
43
  .string()
40
44
  .optional()
41
- .describe(`An optional service worker id to evaluate the script in.`),
45
+ .describe(`The optional service worker id to evaluate the script in. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a service worker.`),
42
46
  }
43
47
  : {}),
44
48
  },
45
49
  handler: async (request, response, context) => {
46
- const { serviceWorkerId, args: uidArgs, function: fnString, pageId, } = request.params;
50
+ const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, } = request.params;
47
51
  if (cliArgs?.categoryExtensions && serviceWorkerId) {
48
52
  if (uidArgs && uidArgs.length > 0) {
49
53
  throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
@@ -52,7 +56,9 @@ Example with arguments: \`(el) => {
52
56
  throw new Error('specify either a pageId or a serviceWorkerId.');
53
57
  }
54
58
  const worker = await getWebWorker(context, serviceWorkerId);
55
- await performEvaluation(worker, fnString, [], response, context);
59
+ await context.getSelectedMcpPage().waitForEventsAfterAction(async () => {
60
+ await performEvaluation(worker, fnString, [], response);
61
+ }, { handleDialog: dialogAction ?? 'accept' });
56
62
  return;
57
63
  }
58
64
  const mcpPage = cliArgs?.experimentalPageIdRouting
@@ -68,7 +74,9 @@ Example with arguments: \`(el) => {
68
74
  args.push(handle);
69
75
  }
70
76
  const evaluatable = await getPageOrFrame(page, frames);
71
- await performEvaluation(evaluatable, fnString, args, response, context);
77
+ await mcpPage.waitForEventsAfterAction(async () => {
78
+ await performEvaluation(evaluatable, fnString, args, response);
79
+ }, { handleDialog: dialogAction ?? 'accept' });
72
80
  }
73
81
  finally {
74
82
  void Promise.allSettled(args.map(arg => arg.dispose()));
@@ -76,19 +84,17 @@ Example with arguments: \`(el) => {
76
84
  },
77
85
  };
78
86
  });
79
- const performEvaluation = async (evaluatable, fnString, args, response, context) => {
87
+ const performEvaluation = async (evaluatable, fnString, args, response) => {
80
88
  const fn = await evaluatable.evaluateHandle(`(${fnString})`);
81
89
  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
- });
90
+ const result = await evaluatable.evaluate(async (fn, ...args) => {
91
+ // @ts-expect-error no types for function fn
92
+ return JSON.stringify(await fn(...args));
93
+ }, fn, ...args);
94
+ response.appendResponseLine('Script ran on page and returned:');
95
+ response.appendResponseLine('```json');
96
+ response.appendResponseLine(`${result}`);
97
+ response.appendResponseLine('```');
92
98
  }
93
99
  finally {
94
100
  void fn.dispose();
@@ -18,6 +18,7 @@ import * as screenshotTools from './screenshot.js';
18
18
  import * as scriptTools from './script.js';
19
19
  import * as slimTools from './slim/tools.js';
20
20
  import * as snapshotTools from './snapshot.js';
21
+ import * as webmcpTools from './webmcp.js';
21
22
  export const createTools = (args) => {
22
23
  const rawTools = args.slim
23
24
  ? Object.values(slimTools)
@@ -36,6 +37,7 @@ export const createTools = (args) => {
36
37
  ...Object.values(screenshotTools),
37
38
  ...Object.values(scriptTools),
38
39
  ...Object.values(snapshotTools),
40
+ ...Object.values(webmcpTools),
39
41
  ];
40
42
  const tools = [];
41
43
  for (const tool of rawTools) {