chrome-devtools-mcp 0.25.0 → 1.0.1

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 (38) hide show
  1. package/README.md +55 -6
  2. package/build/src/DevtoolsUtils.js +13 -0
  3. package/build/src/HeapSnapshotManager.js +26 -2
  4. package/build/src/McpContext.js +3 -0
  5. package/build/src/McpResponse.js +51 -21
  6. package/build/src/ToolHandler.js +217 -0
  7. package/build/src/WaitForHelper.js +18 -4
  8. package/build/src/bin/check-latest-version.js +25 -1
  9. package/build/src/bin/chrome-devtools-cli-options.js +38 -2
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/bin/chrome-devtools-mcp-main.js +4 -3
  12. package/build/src/bin/chrome-devtools.js +0 -2
  13. package/build/src/daemon/client.js +12 -6
  14. package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
  15. package/build/src/index.js +11 -164
  16. package/build/src/telemetry/ClearcutLogger.js +34 -118
  17. package/build/src/telemetry/errors.js +18 -0
  18. package/build/src/telemetry/flagUtils.js +4 -3
  19. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  20. package/build/src/telemetry/persistence.js +20 -2
  21. package/build/src/telemetry/transformation.js +134 -0
  22. package/build/src/telemetry/types.js +0 -8
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +140 -857
  24. package/build/src/third_party/bundled-packages.json +3 -3
  25. package/build/src/third_party/devtools-formatter-worker.js +475 -146
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +39 -44
  27. package/build/src/third_party/index.js +4055 -30401
  28. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
  30. package/build/src/tools/ToolDefinition.js +1 -1
  31. package/build/src/tools/emulation.js +3 -2
  32. package/build/src/tools/input.js +46 -16
  33. package/build/src/tools/lighthouse.js +7 -7
  34. package/build/src/tools/memory.js +24 -0
  35. package/build/src/tools/script.js +32 -10
  36. package/build/src/version.js +1 -1
  37. package/package.json +10 -7
  38. package/build/src/telemetry/metricUtils.js +0 -15
@@ -64,7 +64,7 @@ export function geolocationTransform(arg) {
64
64
  if (!arg) {
65
65
  return undefined;
66
66
  }
67
- const [latitude, longitude] = arg.split('x').map(Number);
67
+ const [latitude, longitude] = arg.split(',').map(Number);
68
68
  return {
69
69
  latitude,
70
70
  longitude,
@@ -33,7 +33,7 @@ export const emulate = definePageTool({
33
33
  .string()
34
34
  .optional()
35
35
  .transform(geolocationTransform)
36
- .describe('Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.'),
36
+ .describe('Geolocation (`<latitude>,<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.'),
37
37
  userAgent: zod
38
38
  .string()
39
39
  .optional()
@@ -49,9 +49,10 @@ export const emulate = definePageTool({
49
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.`),
50
50
  },
51
51
  blockedByDialog: true,
52
- handler: async (request, _response, context) => {
52
+ handler: async (request, response, context) => {
53
53
  const page = request.page;
54
54
  await context.emulate(request.params, page.pptrPage);
55
+ response.appendResponseLine('Emulation configured successfully');
55
56
  },
56
57
  });
57
58
  //# sourceMappingURL=emulation.js.map
@@ -85,7 +85,7 @@ export const click = definePageTool({
85
85
  const aXNode = request.page.getAXNodeByUid(uid);
86
86
  const shouldSelectNativeOption = !request.params.dblClick && aXNode?.role === 'option';
87
87
  try {
88
- await request.page.waitForEventsAfterAction(async () => {
88
+ const result = await request.page.waitForEventsAfterAction(async () => {
89
89
  if (shouldSelectNativeOption &&
90
90
  (await selectNativeSelectOption(handle))) {
91
91
  return;
@@ -97,6 +97,7 @@ export const click = definePageTool({
97
97
  response.appendResponseLine(request.params.dblClick
98
98
  ? `Successfully double clicked on the element`
99
99
  : `Successfully clicked on the element`);
100
+ response.attachWaitForResult(result);
100
101
  if (request.params.includeSnapshot) {
101
102
  response.includeSnapshot();
102
103
  }
@@ -126,14 +127,15 @@ export const clickAt = definePageTool({
126
127
  blockedByDialog: true,
127
128
  handler: async (request, response) => {
128
129
  const page = request.page;
129
- await page.waitForEventsAfterAction(async () => {
130
+ const result = await page.waitForEventsAfterAction(async () => {
130
131
  await page.pptrPage.mouse.click(request.params.x, request.params.y, {
131
- clickCount: request.params.dblClick ? 2 : 1,
132
+ count: request.params.dblClick ? 2 : 1,
132
133
  });
133
134
  });
134
135
  response.appendResponseLine(request.params.dblClick
135
136
  ? `Successfully double clicked at the coordinates`
136
137
  : `Successfully clicked at the coordinates`);
138
+ response.attachWaitForResult(result);
137
139
  if (request.params.includeSnapshot) {
138
140
  response.includeSnapshot();
139
141
  }
@@ -157,10 +159,11 @@ export const hover = definePageTool({
157
159
  const uid = request.params.uid;
158
160
  const handle = await request.page.getElementByUid(uid);
159
161
  try {
160
- await request.page.waitForEventsAfterAction(async () => {
162
+ const result = await request.page.waitForEventsAfterAction(async () => {
161
163
  await handle.asLocator().hover();
162
164
  });
163
165
  response.appendResponseLine(`Successfully hovered over the element`);
166
+ response.attachWaitForResult(result);
164
167
  if (request.params.includeSnapshot) {
165
168
  response.includeSnapshot();
166
169
  }
@@ -220,10 +223,27 @@ async function fillFormElement(uid, value, context, page) {
220
223
  await selectOption(handle, aXNode, value);
221
224
  }
222
225
  else {
223
- // Increase timeout for longer input values.
224
- const timeoutPerChar = 10; // ms
225
- const fillTimeout = page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
226
- await handle.asLocator().setTimeout(fillTimeout).fill(value);
226
+ const isToggle = await handle.evaluate(el => {
227
+ if (el instanceof HTMLInputElement) {
228
+ return el.type === 'checkbox' || el.type === 'radio';
229
+ }
230
+ const role = el.getAttribute('role');
231
+ return role === 'checkbox' || role === 'radio' || role === 'switch';
232
+ });
233
+ if (isToggle) {
234
+ if (['true', 'false'].includes(value)) {
235
+ await handle.asLocator().fill(value === 'true');
236
+ }
237
+ else {
238
+ throw new Error(`Checkboxes, radio boxes and toggles require "true" or "false" value, but ${value} was used`);
239
+ }
240
+ }
241
+ else {
242
+ // Increase timeout for longer input values.
243
+ const timeoutPerChar = 10; // ms
244
+ const fillTimeout = page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
245
+ await handle.asLocator().setTimeout(fillTimeout).fill(value);
246
+ }
227
247
  }
228
248
  }
229
249
  catch (error) {
@@ -244,16 +264,19 @@ export const fill = definePageTool({
244
264
  uid: zod
245
265
  .string()
246
266
  .describe('The uid of an element on the page from the page content snapshot'),
247
- value: zod.string().describe('The value to fill in'),
267
+ value: zod
268
+ .string()
269
+ .describe('The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.'),
248
270
  includeSnapshot: includeSnapshotSchema,
249
271
  },
250
272
  blockedByDialog: true,
251
273
  handler: async (request, response, context) => {
252
274
  const page = request.page;
253
- await page.waitForEventsAfterAction(async () => {
275
+ const result = await page.waitForEventsAfterAction(async () => {
254
276
  await fillFormElement(request.params.uid, request.params.value, context, page);
255
277
  });
256
278
  response.appendResponseLine(`Successfully filled out the element`);
279
+ response.attachWaitForResult(result);
257
280
  if (request.params.includeSnapshot) {
258
281
  response.includeSnapshot();
259
282
  }
@@ -273,13 +296,14 @@ export const typeText = definePageTool({
273
296
  blockedByDialog: true,
274
297
  handler: async (request, response) => {
275
298
  const page = request.page;
276
- await page.waitForEventsAfterAction(async () => {
299
+ const result = await page.waitForEventsAfterAction(async () => {
277
300
  await page.pptrPage.keyboard.type(request.params.text);
278
301
  if (request.params.submitKey) {
279
302
  await page.pptrPage.keyboard.press(request.params.submitKey);
280
303
  }
281
304
  });
282
305
  response.appendResponseLine(`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`);
306
+ response.attachWaitForResult(result);
283
307
  },
284
308
  });
285
309
  export const drag = definePageTool({
@@ -299,12 +323,13 @@ export const drag = definePageTool({
299
323
  const fromHandle = await request.page.getElementByUid(request.params.from_uid);
300
324
  const toHandle = await request.page.getElementByUid(request.params.to_uid);
301
325
  try {
302
- await request.page.waitForEventsAfterAction(async () => {
326
+ const result = await request.page.waitForEventsAfterAction(async () => {
303
327
  await fromHandle.drag(toHandle);
304
328
  await new Promise(resolve => setTimeout(resolve, 50));
305
329
  await toHandle.drop(fromHandle);
306
330
  });
307
331
  response.appendResponseLine(`Successfully dragged an element`);
332
+ response.attachWaitForResult(result);
308
333
  if (request.params.includeSnapshot) {
309
334
  response.includeSnapshot();
310
335
  }
@@ -317,7 +342,7 @@ export const drag = definePageTool({
317
342
  });
318
343
  export const fillForm = definePageTool({
319
344
  name: 'fill_form',
320
- description: `Fill out multiple form elements at once`,
345
+ description: `Fill out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS prefer this tool over multiple individual 'fill' or 'click' calls when interacting with forms. It is significantly faster, more reliable, and reduces turn count. Example: Fill username, password, and check "Remember Me" in one call.`,
321
346
  annotations: {
322
347
  category: ToolCategory.INPUT,
323
348
  readOnlyHint: false,
@@ -328,7 +353,9 @@ export const fillForm = definePageTool({
328
353
  // eslint-disable-next-line @local/enforce-zod-schema
329
354
  zod.object({
330
355
  uid: zod.string().describe('The uid of the element to fill out'),
331
- value: zod.string().describe('Value for the element'),
356
+ value: zod
357
+ .string()
358
+ .describe('Value for the element. "true" or "false" for checkboxes and toggles, "true" for radio buttons.'),
332
359
  }))
333
360
  .describe('Elements from snapshot to fill out.'),
334
361
  includeSnapshot: includeSnapshotSchema,
@@ -336,12 +363,14 @@ export const fillForm = definePageTool({
336
363
  blockedByDialog: true,
337
364
  handler: async (request, response, context) => {
338
365
  const page = request.page;
366
+ let lastResult = {};
339
367
  for (const element of request.params.elements) {
340
- await page.waitForEventsAfterAction(async () => {
368
+ lastResult = await page.waitForEventsAfterAction(async () => {
341
369
  await fillFormElement(element.uid, element.value, context, page);
342
370
  });
343
371
  }
344
372
  response.appendResponseLine(`Successfully filled out the form`);
373
+ response.attachWaitForResult(lastResult);
345
374
  if (request.params.includeSnapshot) {
346
375
  response.includeSnapshot();
347
376
  }
@@ -413,7 +442,7 @@ export const pressKey = definePageTool({
413
442
  const page = request.page;
414
443
  const tokens = parseKey(request.params.key);
415
444
  const [key, ...modifiers] = tokens;
416
- await page.waitForEventsAfterAction(async () => {
445
+ const result = await page.waitForEventsAfterAction(async () => {
417
446
  for (const modifier of modifiers) {
418
447
  await page.pptrPage.keyboard.down(modifier);
419
448
  }
@@ -423,6 +452,7 @@ export const pressKey = definePageTool({
423
452
  }
424
453
  });
425
454
  response.appendResponseLine(`Successfully pressed key: ${request.params.key}`);
455
+ response.attachWaitForResult(result);
426
456
  if (request.params.includeSnapshot) {
427
457
  response.includeSnapshot();
428
458
  }
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import path from 'node:path';
7
- import { snapshot, navigation, generateReport, zod, agenticBrowsingConfig, } from '../third_party/index.js';
7
+ import { snapshot, navigation, generateReport, zod, } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { startTrace } from './performance.js';
10
10
  import { definePageTool } from './ToolDefinition.js';
@@ -67,17 +67,17 @@ export const lighthouseAudit = definePageTool({
67
67
  disabled: false,
68
68
  };
69
69
  }
70
- const options = {
71
- flags,
72
- config: agenticBrowsingConfig,
73
- };
74
70
  let result;
75
71
  try {
76
72
  if (mode === 'navigation') {
77
- result = await navigation(page.pptrPage, page.pptrPage.url(), options);
73
+ result = await navigation(page.pptrPage, page.pptrPage.url(), {
74
+ flags,
75
+ });
78
76
  }
79
77
  else {
80
- result = await snapshot(page.pptrPage, options);
78
+ result = await snapshot(page.pptrPage, {
79
+ flags,
80
+ });
81
81
  }
82
82
  if (!result) {
83
83
  throw new Error('Lighthouse audit failed.');
@@ -103,4 +103,28 @@ export const getNodesByClass = defineTool({
103
103
  });
104
104
  },
105
105
  });
106
+ export const getNodeRetainers = defineTool({
107
+ name: 'get_node_retainers',
108
+ description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID.',
109
+ annotations: {
110
+ category: ToolCategory.MEMORY,
111
+ readOnlyHint: true,
112
+ conditions: ['experimentalMemory'],
113
+ },
114
+ blockedByDialog: false,
115
+ schema: {
116
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
117
+ nodeId: zod.number().describe('The stable node ID to get retainers for.'),
118
+ pageIdx: zod.number().optional().describe('The page index for pagination.'),
119
+ pageSize: zod.number().optional().describe('The page size for pagination.'),
120
+ },
121
+ handler: async (request, response, context) => {
122
+ context.validatePath(request.params.filePath);
123
+ const retainers = await context.getHeapSnapshotRetainers(request.params.filePath, request.params.nodeId);
124
+ response.setHeapSnapshotNodes(retainers, {
125
+ pageIdx: request.params.pageIdx,
126
+ pageSize: request.params.pageSize,
127
+ });
128
+ },
129
+ });
106
130
  //# sourceMappingURL=memory.js.map
@@ -32,6 +32,10 @@ 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
+ filePath: zod
36
+ .string()
37
+ .optional()
38
+ .describe('The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.'),
35
39
  dialogAction: zod
36
40
  .string()
37
41
  .optional()
@@ -48,7 +52,8 @@ Example with arguments: \`(el) => {
48
52
  },
49
53
  blockedByDialog: true,
50
54
  handler: async (request, response, context) => {
51
- const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, } = request.params;
55
+ const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, filePath, } = request.params;
56
+ context.validatePath(filePath);
52
57
  if (cliArgs?.categoryExtensions && serviceWorkerId) {
53
58
  if (uidArgs && uidArgs.length > 0) {
54
59
  throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
@@ -57,9 +62,15 @@ Example with arguments: \`(el) => {
57
62
  throw new Error('specify either a pageId or a serviceWorkerId.');
58
63
  }
59
64
  const worker = await getWebWorker(context, serviceWorkerId);
60
- await context.getSelectedMcpPage().waitForEventsAfterAction(async () => {
61
- await performEvaluation(worker, fnString, [], response);
65
+ const result = await context
66
+ .getSelectedMcpPage()
67
+ .waitForEventsAfterAction(async () => {
68
+ await performEvaluation(worker, fnString, [], response, {
69
+ filePath,
70
+ context,
71
+ });
62
72
  }, { handleDialog: dialogAction ?? 'accept' });
73
+ response.attachWaitForResult(result);
63
74
  return;
64
75
  }
65
76
  const mcpPage = cliArgs?.experimentalPageIdRouting
@@ -75,9 +86,13 @@ Example with arguments: \`(el) => {
75
86
  args.push(handle);
76
87
  }
77
88
  const evaluatable = await getPageOrFrame(page, frames);
78
- await mcpPage.waitForEventsAfterAction(async () => {
79
- await performEvaluation(evaluatable, fnString, args, response);
89
+ const result = await mcpPage.waitForEventsAfterAction(async () => {
90
+ await performEvaluation(evaluatable, fnString, args, response, {
91
+ filePath,
92
+ context,
93
+ });
80
94
  }, { handleDialog: dialogAction ?? 'accept' });
95
+ response.attachWaitForResult(result);
81
96
  }
82
97
  finally {
83
98
  void Promise.allSettled(args.map(arg => arg.dispose()));
@@ -85,17 +100,24 @@ Example with arguments: \`(el) => {
85
100
  },
86
101
  };
87
102
  });
88
- const performEvaluation = async (evaluatable, fnString, args, response) => {
103
+ const performEvaluation = async (evaluatable, fnString, args, response, options) => {
89
104
  const fn = await evaluatable.evaluateHandle(`(${fnString})`);
90
105
  try {
91
106
  const result = await evaluatable.evaluate(async (fn, ...args) => {
92
107
  // @ts-expect-error no types for function fn
93
108
  return JSON.stringify(await fn(...args));
94
109
  }, fn, ...args);
95
- response.appendResponseLine('Script ran on page and returned:');
96
- response.appendResponseLine('```json');
97
- response.appendResponseLine(`${result}`);
98
- response.appendResponseLine('```');
110
+ if (options?.filePath) {
111
+ const data = new TextEncoder().encode(result ?? 'undefined');
112
+ const { filename } = await options.context.saveFile(data, options.filePath, '.json');
113
+ response.appendResponseLine(`Script ran on page. Output saved to ${filename}.`);
114
+ }
115
+ else {
116
+ response.appendResponseLine('Script ran on page and returned:');
117
+ response.appendResponseLine('```json');
118
+ response.appendResponseLine(`${result}`);
119
+ response.appendResponseLine('```');
120
+ }
99
121
  }
100
122
  finally {
101
123
  void fn.dispose();
@@ -5,6 +5,6 @@
5
5
  */
6
6
  // If moved update release-please config
7
7
  // x-release-please-start-version
8
- export const VERSION = '0.25.0';
8
+ export const VERSION = '1.0.1';
9
9
  // x-release-please-end
10
10
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.25.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp",
49
49
  "devDependencies": {
50
50
  "@eslint/js": "^9.35.0",
51
- "@google/genai": "^1.37.0",
51
+ "@google/genai": "^2.0.1",
52
52
  "@modelcontextprotocol/sdk": "1.29.0",
53
53
  "@rollup/plugin-commonjs": "^29.0.0",
54
54
  "@rollup/plugin-json": "^6.1.0",
@@ -62,21 +62,21 @@
62
62
  "@types/yargs": "^17.0.33",
63
63
  "@typescript-eslint/eslint-plugin": "^8.43.0",
64
64
  "@typescript-eslint/parser": "^8.43.0",
65
- "chrome-devtools-frontend": "1.0.1625079",
65
+ "chrome-devtools-frontend": "1.0.1631386",
66
66
  "core-js": "3.49.0",
67
67
  "debug": "4.4.3",
68
68
  "eslint": "^9.35.0",
69
69
  "eslint-import-resolver-typescript": "^4.4.4",
70
70
  "eslint-plugin-import": "^2.32.0",
71
71
  "globals": "^17.0.0",
72
- "lighthouse": "13.2.0",
72
+ "lighthouse": "13.3.0",
73
73
  "prettier": "^3.6.2",
74
- "puppeteer": "24.43.0",
75
- "rollup": "4.60.2",
74
+ "puppeteer": "25.0.4",
75
+ "rollup": "4.60.4",
76
76
  "rollup-plugin-cleanup": "^3.2.1",
77
77
  "rollup-plugin-license": "^3.6.0",
78
78
  "semver": "^7.7.4",
79
- "sinon": "^21.0.0",
79
+ "sinon": "^22.0.0",
80
80
  "typescript": "^6.0.2",
81
81
  "typescript-eslint": "^8.43.0",
82
82
  "urlpattern-polyfill": "^10.1.0",
@@ -84,5 +84,8 @@
84
84
  },
85
85
  "engines": {
86
86
  "node": "^20.19.0 || ^22.12.0 || >=23"
87
+ },
88
+ "overrides": {
89
+ "puppeteer-core": "$puppeteer"
87
90
  }
88
91
  }
@@ -1,15 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- const LATENCY_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000];
7
- export function bucketizeLatency(latencyMs) {
8
- for (const bucket of LATENCY_BUCKETS) {
9
- if (latencyMs <= bucket) {
10
- return bucket;
11
- }
12
- }
13
- return LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1];
14
- }
15
- //# sourceMappingURL=metricUtils.js.map