chrome-devtools-mcp 0.26.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 (33) hide show
  1. package/README.md +54 -5
  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 +30 -1
  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 +37 -1
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/daemon/client.js +12 -6
  12. package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
  13. package/build/src/index.js +3 -1
  14. package/build/src/telemetry/ClearcutLogger.js +8 -119
  15. package/build/src/telemetry/errors.js +4 -0
  16. package/build/src/telemetry/flagUtils.js +4 -3
  17. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  18. package/build/src/telemetry/persistence.js +20 -2
  19. package/build/src/telemetry/transformation.js +134 -0
  20. package/build/src/third_party/THIRD_PARTY_NOTICES +2 -717
  21. package/build/src/third_party/bundled-packages.json +2 -2
  22. package/build/src/third_party/devtools-formatter-worker.js +445 -114
  23. package/build/src/third_party/devtools-heap-snapshot-worker.js +0 -3
  24. package/build/src/third_party/index.js +3255 -30087
  25. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  26. package/build/src/tools/ToolDefinition.js +1 -1
  27. package/build/src/tools/emulation.js +3 -2
  28. package/build/src/tools/input.js +18 -9
  29. package/build/src/tools/memory.js +24 -0
  30. package/build/src/tools/script.js +32 -10
  31. package/build/src/version.js +1 -1
  32. package/package.json +7 -4
  33. package/build/src/telemetry/metricUtils.js +0 -15
@@ -0,0 +1,4 @@
1
+ # Back UI Navigation Will Skip Ad
2
+
3
+ An ad-related entry was found in the session history. If the user navigates back
4
+ via the browser UI, this ad entry will be skipped.
@@ -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
  }
@@ -269,10 +272,11 @@ export const fill = definePageTool({
269
272
  blockedByDialog: true,
270
273
  handler: async (request, response, context) => {
271
274
  const page = request.page;
272
- await page.waitForEventsAfterAction(async () => {
275
+ const result = await page.waitForEventsAfterAction(async () => {
273
276
  await fillFormElement(request.params.uid, request.params.value, context, page);
274
277
  });
275
278
  response.appendResponseLine(`Successfully filled out the element`);
279
+ response.attachWaitForResult(result);
276
280
  if (request.params.includeSnapshot) {
277
281
  response.includeSnapshot();
278
282
  }
@@ -292,13 +296,14 @@ export const typeText = definePageTool({
292
296
  blockedByDialog: true,
293
297
  handler: async (request, response) => {
294
298
  const page = request.page;
295
- await page.waitForEventsAfterAction(async () => {
299
+ const result = await page.waitForEventsAfterAction(async () => {
296
300
  await page.pptrPage.keyboard.type(request.params.text);
297
301
  if (request.params.submitKey) {
298
302
  await page.pptrPage.keyboard.press(request.params.submitKey);
299
303
  }
300
304
  });
301
305
  response.appendResponseLine(`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`);
306
+ response.attachWaitForResult(result);
302
307
  },
303
308
  });
304
309
  export const drag = definePageTool({
@@ -318,12 +323,13 @@ export const drag = definePageTool({
318
323
  const fromHandle = await request.page.getElementByUid(request.params.from_uid);
319
324
  const toHandle = await request.page.getElementByUid(request.params.to_uid);
320
325
  try {
321
- await request.page.waitForEventsAfterAction(async () => {
326
+ const result = await request.page.waitForEventsAfterAction(async () => {
322
327
  await fromHandle.drag(toHandle);
323
328
  await new Promise(resolve => setTimeout(resolve, 50));
324
329
  await toHandle.drop(fromHandle);
325
330
  });
326
331
  response.appendResponseLine(`Successfully dragged an element`);
332
+ response.attachWaitForResult(result);
327
333
  if (request.params.includeSnapshot) {
328
334
  response.includeSnapshot();
329
335
  }
@@ -357,12 +363,14 @@ export const fillForm = definePageTool({
357
363
  blockedByDialog: true,
358
364
  handler: async (request, response, context) => {
359
365
  const page = request.page;
366
+ let lastResult = {};
360
367
  for (const element of request.params.elements) {
361
- await page.waitForEventsAfterAction(async () => {
368
+ lastResult = await page.waitForEventsAfterAction(async () => {
362
369
  await fillFormElement(element.uid, element.value, context, page);
363
370
  });
364
371
  }
365
372
  response.appendResponseLine(`Successfully filled out the form`);
373
+ response.attachWaitForResult(lastResult);
366
374
  if (request.params.includeSnapshot) {
367
375
  response.includeSnapshot();
368
376
  }
@@ -434,7 +442,7 @@ export const pressKey = definePageTool({
434
442
  const page = request.page;
435
443
  const tokens = parseKey(request.params.key);
436
444
  const [key, ...modifiers] = tokens;
437
- await page.waitForEventsAfterAction(async () => {
445
+ const result = await page.waitForEventsAfterAction(async () => {
438
446
  for (const modifier of modifiers) {
439
447
  await page.pptrPage.keyboard.down(modifier);
440
448
  }
@@ -444,6 +452,7 @@ export const pressKey = definePageTool({
444
452
  }
445
453
  });
446
454
  response.appendResponseLine(`Successfully pressed key: ${request.params.key}`);
455
+ response.attachWaitForResult(result);
447
456
  if (request.params.includeSnapshot) {
448
457
  response.includeSnapshot();
449
458
  }
@@ -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.26.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.26.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,7 +62,7 @@
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.1626840",
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",
@@ -71,8 +71,8 @@
71
71
  "globals": "^17.0.0",
72
72
  "lighthouse": "13.3.0",
73
73
  "prettier": "^3.6.2",
74
- "puppeteer": "24.43.0",
75
- "rollup": "4.60.3",
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",
@@ -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