chrome-devtools-mcp 0.26.0 → 1.1.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 (45) hide show
  1. package/README.md +59 -10
  2. package/build/src/DevtoolsUtils.js +14 -1
  3. package/build/src/HeapSnapshotManager.js +42 -18
  4. package/build/src/McpContext.js +61 -23
  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 +81 -39
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/bin/chrome-devtools-mcp-main.js +38 -0
  12. package/build/src/browser.js +36 -2
  13. package/build/src/daemon/client.js +12 -6
  14. package/build/src/daemon/daemon.js +62 -5
  15. package/build/src/formatters/HeapSnapshotFormatter.js +30 -9
  16. package/build/src/index.js +3 -1
  17. package/build/src/telemetry/ClearcutLogger.js +8 -119
  18. package/build/src/telemetry/errors.js +4 -0
  19. package/build/src/telemetry/flagUtils.js +4 -3
  20. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  21. package/build/src/telemetry/persistence.js +20 -2
  22. package/build/src/telemetry/transformation.js +134 -0
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +4 -719
  24. package/build/src/third_party/bundled-packages.json +2 -2
  25. package/build/src/third_party/devtools-formatter-worker.js +447 -114
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +2 -3
  27. package/build/src/third_party/index.js +3443 -30153
  28. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  29. package/build/src/tools/ToolDefinition.js +2 -2
  30. package/build/src/tools/emulation.js +28 -2
  31. package/build/src/tools/extensions.js +2 -0
  32. package/build/src/tools/input.js +19 -10
  33. package/build/src/tools/lighthouse.js +1 -1
  34. package/build/src/tools/memory.js +39 -17
  35. package/build/src/tools/network.js +2 -2
  36. package/build/src/tools/performance.js +9 -6
  37. package/build/src/tools/screencast.js +1 -1
  38. package/build/src/tools/screenshot.js +1 -1
  39. package/build/src/tools/script.js +32 -10
  40. package/build/src/tools/snapshot.js +1 -1
  41. package/build/src/trace-processing/parse.js +2 -2
  42. package/build/src/utils/files.js +43 -0
  43. package/build/src/version.js +1 -1
  44. package/package.json +7 -4
  45. 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.
@@ -30,7 +30,7 @@ export function definePageTool(definition) {
30
30
  }
31
31
  export const CLOSE_PAGE_ERROR = 'The last open page cannot be closed. It is fine to keep it open.';
32
32
  export const pageIdSchema = {
33
- pageId: zod.number().optional().describe('Targets a specific page by ID.'),
33
+ pageId: zod.number().describe('Targets a specific page by ID.'),
34
34
  };
35
35
  export const timeoutSchema = {
36
36
  timeout: zod
@@ -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,
@@ -7,6 +7,26 @@
7
7
  import { zod, PredefinedNetworkConditions } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { definePageTool, geolocationTransform, viewportTransform, } from './ToolDefinition.js';
10
+ function headerStringTransform(value) {
11
+ if (value === undefined) {
12
+ return undefined;
13
+ }
14
+ if (value === '') {
15
+ return {};
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(value);
19
+ if (typeof parsed !== 'object' ||
20
+ parsed === null ||
21
+ Array.isArray(parsed)) {
22
+ throw new Error('Headers must be a JSON object');
23
+ }
24
+ return parsed;
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Invalid JSON for headers: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }
10
30
  const throttlingOptions = [
11
31
  'Offline',
12
32
  ...Object.keys(PredefinedNetworkConditions),
@@ -33,7 +53,7 @@ export const emulate = definePageTool({
33
53
  .string()
34
54
  .optional()
35
55
  .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.'),
56
+ .describe('Geolocation (`<latitude>,<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.'),
37
57
  userAgent: zod
38
58
  .string()
39
59
  .optional()
@@ -47,11 +67,17 @@ export const emulate = definePageTool({
47
67
  .optional()
48
68
  .transform(viewportTransform)
49
69
  .describe(`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`),
70
+ extraHttpHeaders: zod
71
+ .string()
72
+ .optional()
73
+ .transform(headerStringTransform)
74
+ .describe('Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.'),
50
75
  },
51
76
  blockedByDialog: true,
52
- handler: async (request, _response, context) => {
77
+ handler: async (request, response, context) => {
53
78
  const page = request.page;
54
79
  await context.emulate(request.params, page.pptrPage);
80
+ response.appendResponseLine('Emulation configured successfully');
55
81
  },
56
82
  });
57
83
  //# sourceMappingURL=emulation.js.map
@@ -21,6 +21,7 @@ export const installExtension = defineTool({
21
21
  blockedByDialog: false,
22
22
  handler: async (request, response, context) => {
23
23
  const { path } = request.params;
24
+ await context.validatePath(path);
24
25
  const id = await context.installExtension(path);
25
26
  response.appendResponseLine(`Extension installed. Id: ${id}`);
26
27
  },
@@ -72,6 +73,7 @@ export const reloadExtension = defineTool({
72
73
  if (!extension) {
73
74
  throw new Error(`Extension with ID ${id} not found.`);
74
75
  }
76
+ await context.validatePath(extension.path);
75
77
  await context.installExtension(extension.path);
76
78
  response.appendResponseLine('Extension reloaded.');
77
79
  },
@@ -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
  }
@@ -385,7 +393,7 @@ export const uploadFile = definePageTool({
385
393
  blockedByDialog: true,
386
394
  handler: async (request, response, context) => {
387
395
  const { uid, filePath } = request.params;
388
- context.validatePath(filePath);
396
+ await context.validatePath(filePath);
389
397
  const handle = (await request.page.getElementByUid(uid));
390
398
  try {
391
399
  try {
@@ -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
  }
@@ -40,7 +40,7 @@ export const lighthouseAudit = definePageTool({
40
40
  ];
41
41
  const formats = ['json', 'html'];
42
42
  const { mode = 'navigation', device = 'desktop', outputDirPath, } = request.params;
43
- context.validatePath(outputDirPath);
43
+ await context.validatePath(outputDirPath);
44
44
  const flags = {
45
45
  onlyCategories: categories,
46
46
  output: formats,
@@ -7,8 +7,8 @@ import { zod } from '../third_party/index.js';
7
7
  import { ensureExtension } from '../utils/files.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { definePageTool, defineTool } from './ToolDefinition.js';
10
- export const takeMemorySnapshot = definePageTool({
11
- name: 'take_memory_snapshot',
10
+ export const takeHeapSnapshot = definePageTool({
11
+ name: 'take_heapsnapshot',
12
12
  description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
13
13
  annotations: {
14
14
  category: ToolCategory.MEMORY,
@@ -22,15 +22,15 @@ export const takeMemorySnapshot = definePageTool({
22
22
  blockedByDialog: true,
23
23
  handler: async (request, response, context) => {
24
24
  const page = request.page;
25
- context.validatePath(request.params.filePath);
25
+ await context.validatePath(request.params.filePath);
26
26
  await page.pptrPage.captureHeapSnapshot({
27
27
  path: ensureExtension(request.params.filePath, '.heapsnapshot'),
28
28
  });
29
29
  response.appendResponseLine(`Heap snapshot saved to ${request.params.filePath}`);
30
30
  },
31
31
  });
32
- export const exploreMemorySnapshot = defineTool({
33
- name: 'load_memory_snapshot',
32
+ export const getHeapSnapshotSummary = defineTool({
33
+ name: 'get_heapsnapshot_summary',
34
34
  description: 'Loads a memory heapsnapshot and returns snapshot summary stats.',
35
35
  annotations: {
36
36
  category: ToolCategory.MEMORY,
@@ -42,14 +42,14 @@ export const exploreMemorySnapshot = defineTool({
42
42
  },
43
43
  blockedByDialog: false,
44
44
  handler: async (request, response, context) => {
45
- context.validatePath(request.params.filePath);
45
+ await context.validatePath(request.params.filePath);
46
46
  const stats = await context.getHeapSnapshotStats(request.params.filePath);
47
47
  const staticData = await context.getHeapSnapshotStaticData(request.params.filePath);
48
48
  response.setHeapSnapshotStats(stats, staticData);
49
49
  },
50
50
  });
51
- export const getMemorySnapshotDetails = defineTool({
52
- name: 'get_memory_snapshot_details',
51
+ export const getHeapSnapshotDetails = defineTool({
52
+ name: 'get_heapsnapshot_details',
53
53
  description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates.',
54
54
  annotations: {
55
55
  category: ToolCategory.MEMORY,
@@ -69,7 +69,7 @@ export const getMemorySnapshotDetails = defineTool({
69
69
  },
70
70
  blockedByDialog: false,
71
71
  handler: async (request, response, context) => {
72
- context.validatePath(request.params.filePath);
72
+ await context.validatePath(request.params.filePath);
73
73
  const aggregates = await context.getHeapSnapshotAggregates(request.params.filePath);
74
74
  response.setHeapSnapshotAggregates(aggregates, {
75
75
  pageIdx: request.params.pageIdx,
@@ -77,9 +77,9 @@ export const getMemorySnapshotDetails = defineTool({
77
77
  });
78
78
  },
79
79
  });
80
- export const getNodesByClass = defineTool({
81
- name: 'get_nodes_by_class',
82
- description: 'Loads a memory heapsnapshot and returns instances of a specific class with their stable IDs.',
80
+ export const getHeapSnapshotClassNodes = defineTool({
81
+ name: 'get_heapsnapshot_class_nodes',
82
+ description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs.',
83
83
  annotations: {
84
84
  category: ToolCategory.MEMORY,
85
85
  readOnlyHint: true,
@@ -87,20 +87,42 @@ export const getNodesByClass = defineTool({
87
87
  },
88
88
  schema: {
89
89
  filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
90
- uid: zod
91
- .number()
92
- .describe('The unique UID for the class, obtained from aggregates listing.'),
90
+ id: zod.number().describe('The ID for the class, obtained from details.'),
93
91
  pageIdx: zod.number().optional().describe('The page index for pagination.'),
94
92
  pageSize: zod.number().optional().describe('The page size for pagination.'),
95
93
  },
96
94
  blockedByDialog: false,
97
95
  handler: async (request, response, context) => {
98
- context.validatePath(request.params.filePath);
99
- const nodes = await context.getHeapSnapshotNodesByUid(request.params.filePath, request.params.uid);
96
+ await context.validatePath(request.params.filePath);
97
+ const nodes = await context.getHeapSnapshotNodesById(request.params.filePath, request.params.id);
100
98
  response.setHeapSnapshotNodes(nodes, {
101
99
  pageIdx: request.params.pageIdx,
102
100
  pageSize: request.params.pageSize,
103
101
  });
104
102
  },
105
103
  });
104
+ export const getHeapSnapshotRetainers = defineTool({
105
+ name: 'get_heapsnapshot_retainers',
106
+ description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID.',
107
+ annotations: {
108
+ category: ToolCategory.MEMORY,
109
+ readOnlyHint: true,
110
+ conditions: ['experimentalMemory'],
111
+ },
112
+ blockedByDialog: false,
113
+ schema: {
114
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
115
+ nodeId: zod.number().describe('The node ID to get retainers for.'),
116
+ pageIdx: zod.number().optional().describe('The page index for pagination.'),
117
+ pageSize: zod.number().optional().describe('The page size for pagination.'),
118
+ },
119
+ handler: async (request, response, context) => {
120
+ await context.validatePath(request.params.filePath);
121
+ const retainers = await context.getHeapSnapshotRetainers(request.params.filePath, request.params.nodeId);
122
+ response.setHeapSnapshotNodes(retainers, {
123
+ pageIdx: request.params.pageIdx,
124
+ pageSize: request.params.pageSize,
125
+ });
126
+ },
127
+ });
106
128
  //# sourceMappingURL=memory.js.map
@@ -96,8 +96,8 @@ export const getNetworkRequest = definePageTool({
96
96
  },
97
97
  blockedByDialog: true,
98
98
  handler: async (request, response, context) => {
99
- context.validatePath(request.params.requestFilePath);
100
- context.validatePath(request.params.responseFilePath);
99
+ await context.validatePath(request.params.requestFilePath);
100
+ await context.validatePath(request.params.responseFilePath);
101
101
  if (request.params.reqid) {
102
102
  response.attachNetworkRequest(request.params.reqid, {
103
103
  requestFilePath: request.params.requestFilePath,
@@ -33,7 +33,7 @@ export const startTrace = definePageTool({
33
33
  },
34
34
  blockedByDialog: true,
35
35
  handler: async (request, response, context) => {
36
- context.validatePath(request.params.filePath);
36
+ await context.validatePath(request.params.filePath);
37
37
  if (context.isRunningPerformanceTrace()) {
38
38
  response.appendResponseLine('Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.');
39
39
  return;
@@ -78,7 +78,7 @@ export const startTrace = definePageTool({
78
78
  }
79
79
  if (request.params.autoStop) {
80
80
  await new Promise(resolve => setTimeout(resolve, 5_000));
81
- await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
81
+ await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
82
82
  }
83
83
  else {
84
84
  response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
@@ -97,12 +97,12 @@ export const stopTrace = definePageTool({
97
97
  },
98
98
  blockedByDialog: true,
99
99
  handler: async (request, response, context) => {
100
- context.validatePath(request.params.filePath);
100
+ await context.validatePath(request.params.filePath);
101
101
  if (!context.isRunningPerformanceTrace()) {
102
102
  return;
103
103
  }
104
104
  const page = request.page;
105
- await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
105
+ await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
106
106
  },
107
107
  });
108
108
  export const analyzeInsight = definePageTool({
@@ -132,7 +132,7 @@ export const analyzeInsight = definePageTool({
132
132
  });
133
133
  async function stopTracingAndAppendOutput(page, response, context, filePath) {
134
134
  try {
135
- const traceEventsBuffer = await page.tracing.stop();
135
+ const traceEventsBuffer = await page.pptrPage.tracing.stop();
136
136
  if (filePath && traceEventsBuffer) {
137
137
  let dataToWrite = traceEventsBuffer;
138
138
  if (filePath.endsWith('.gz')) {
@@ -150,7 +150,10 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
150
150
  const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.json.gz' : '.json');
151
151
  response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
152
152
  }
153
- const result = await parseRawTraceBuffer(traceEventsBuffer);
153
+ const result = await parseRawTraceBuffer(traceEventsBuffer, {
154
+ cpuThrottling: page.cpuThrottlingRate,
155
+ networkThrottling: page.networkConditions ?? undefined,
156
+ });
154
157
  response.appendResponseLine('The performance trace has been stopped.');
155
158
  if (traceResultIsSuccess(result)) {
156
159
  if (context.isCruxEnabled()) {
@@ -31,7 +31,7 @@ export const startScreencast = definePageTool(args => ({
31
31
  },
32
32
  blockedByDialog: false,
33
33
  handler: async (request, response, context) => {
34
- context.validatePath(request.params.filePath);
34
+ await context.validatePath(request.params.filePath);
35
35
  if (context.getScreenRecorder() !== null) {
36
36
  response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
37
37
  return;
@@ -40,7 +40,7 @@ export const screenshot = definePageTool({
40
40
  },
41
41
  blockedByDialog: true,
42
42
  handler: async (request, response, context) => {
43
- context.validatePath(request.params.filePath);
43
+ await context.validatePath(request.params.filePath);
44
44
  if (request.params.uid && request.params.fullPage) {
45
45
  throw new Error('Providing both "uid" and "fullPage" is not allowed.');
46
46
  }
@@ -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
+ await 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();
@@ -28,7 +28,7 @@ in the DevTools Elements panel (if any).`,
28
28
  },
29
29
  blockedByDialog: true,
30
30
  handler: async (request, response, context) => {
31
- context.validatePath(request.params.filePath);
31
+ await context.validatePath(request.params.filePath);
32
32
  response.includeSnapshot({
33
33
  verbose: request.params.verbose ?? false,
34
34
  filePath: request.params.filePath,
@@ -9,7 +9,7 @@ const engine = DevTools.TraceEngine.TraceModel.Model.createWithAllHandlers();
9
9
  export function traceResultIsSuccess(x) {
10
10
  return 'parsedTrace' in x;
11
11
  }
12
- export async function parseRawTraceBuffer(buffer) {
12
+ export async function parseRawTraceBuffer(buffer, metadata) {
13
13
  engine.resetProcessor();
14
14
  if (!buffer) {
15
15
  return {
@@ -25,7 +25,7 @@ export async function parseRawTraceBuffer(buffer) {
25
25
  try {
26
26
  const data = JSON.parse(asString);
27
27
  const events = Array.isArray(data) ? data : data.traceEvents;
28
- await engine.parse(events);
28
+ await engine.parse(events, { metadata });
29
29
  const parsedTrace = engine.parsedTrace();
30
30
  if (!parsedTrace) {
31
31
  return {
@@ -15,4 +15,47 @@ export function ensureExtension(filepath, extension) {
15
15
  const ext = path.extname(filepath);
16
16
  return filepath.slice(0, filepath.length - ext.length) + extension;
17
17
  }
18
+ export async function resolveCanonicalPath(filePath) {
19
+ const absolutePath = path.resolve(filePath);
20
+ try {
21
+ // Get the true canonical path, resolving all symlinks.
22
+ return await fs.realpath(absolutePath);
23
+ }
24
+ catch (err) {
25
+ if (err &&
26
+ typeof err === 'object' &&
27
+ 'code' in err &&
28
+ err.code === 'ENOENT') {
29
+ // Find the nearest existing ancestor directory on the filesystem.
30
+ let current = absolutePath;
31
+ const missingSegments = [];
32
+ while (true) {
33
+ const parent = path.dirname(current);
34
+ if (parent === current) {
35
+ // Reached root directory but still couldn't resolve anything.
36
+ throw err;
37
+ }
38
+ try {
39
+ const canonicalParent = await fs.realpath(parent);
40
+ return path.join(canonicalParent, path.basename(current), ...missingSegments);
41
+ }
42
+ catch (parentErr) {
43
+ if (parentErr &&
44
+ typeof parentErr === 'object' &&
45
+ 'code' in parentErr &&
46
+ parentErr.code === 'ENOENT') {
47
+ missingSegments.unshift(path.basename(current));
48
+ current = parent;
49
+ }
50
+ else {
51
+ throw parentErr;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ else {
57
+ throw err;
58
+ }
59
+ }
60
+ }
18
61
  //# sourceMappingURL=files.js.map
@@ -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.1.0';
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.1.0",
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.1632065",
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.1.0",
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
  }