chrome-devtools-mcp 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -121,9 +121,9 @@ To use the Chrome DevTools MCP server follow the instructions from <a href="http
121
121
  "chrome-devtools": {
122
122
  "command": "npx",
123
123
  "args": [
124
+ "-y",
124
125
  "chrome-devtools-mcp@latest",
125
- "--browser-url=http://127.0.0.1:9222",
126
- "-y"
126
+ "--browser-url=http://127.0.0.1:9222"
127
127
  ]
128
128
  }
129
129
  }
@@ -514,12 +514,14 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
514
514
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
515
515
  - [`screencast_start`](docs/tool-reference.md#screencast_start)
516
516
  - [`screencast_stop`](docs/tool-reference.md#screencast_stop)
517
- - **Memory** (9 tools)
517
+ - **Memory** (11 tools)
518
518
  - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
519
519
  - [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
520
+ - [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
520
521
  - [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
521
522
  - [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
522
523
  - [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
524
+ - [`get_heapsnapshot_duplicate_strings`](docs/tool-reference.md#get_heapsnapshot_duplicate_strings)
523
525
  - [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
524
526
  - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
525
527
  - [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { DevTools } from './third_party/index.js';
9
9
  import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
10
10
  export class HeapSnapshotManager {
11
+ #snapshotIdGenerator = createIdGenerator();
11
12
  #snapshots = new Map();
12
13
  async getSnapshot(filePath) {
13
14
  const absolutePath = path.resolve(filePath);
@@ -15,7 +16,8 @@ export class HeapSnapshotManager {
15
16
  if (cached) {
16
17
  return cached.snapshot;
17
18
  }
18
- const { snapshot, worker } = await this.#loadSnapshot(absolutePath);
19
+ const uid = this.#snapshotIdGenerator();
20
+ const { snapshot, worker } = await this.#loadSnapshot(absolutePath, uid);
19
21
  this.#snapshots.set(absolutePath, {
20
22
  snapshot,
21
23
  worker,
@@ -100,6 +102,38 @@ export class HeapSnapshotManager {
100
102
  const provider = snapshot.createEdgesProvider(nodeIndex);
101
103
  return await provider.serializeItemsRange(0, Infinity);
102
104
  }
105
+ async getClassDiffs(baseFilePath, currentFilePath) {
106
+ const rawDiffs = await this.#getSortedRawClassDiffs(baseFilePath, currentFilePath);
107
+ return rawDiffs.map(rawDiff => ({
108
+ className: rawDiff.name,
109
+ addedCount: rawDiff.addedCount,
110
+ removedCount: rawDiff.removedCount,
111
+ countDelta: rawDiff.countDelta,
112
+ addedSize: rawDiff.addedSize,
113
+ removedSize: rawDiff.removedSize,
114
+ sizeDelta: rawDiff.sizeDelta,
115
+ }));
116
+ }
117
+ async getDetailedClassDiff(baseFilePath, currentFilePath, classIndex) {
118
+ const classDiffs = await this.#getSortedRawClassDiffs(baseFilePath, currentFilePath);
119
+ const rawDiff = classDiffs[classIndex];
120
+ if (!rawDiff) {
121
+ throw new Error(`Invalid classIndex: ${classIndex}. Total classes with changes: ${classDiffs.length}`);
122
+ }
123
+ return {
124
+ className: rawDiff.name,
125
+ addedCount: rawDiff.addedCount,
126
+ removedCount: rawDiff.removedCount,
127
+ countDelta: rawDiff.countDelta,
128
+ addedSize: rawDiff.addedSize,
129
+ removedSize: rawDiff.removedSize,
130
+ sizeDelta: rawDiff.sizeDelta,
131
+ addedIds: rawDiff.addedIds ?? [],
132
+ addedSelfSizes: rawDiff.addedSelfSizes ?? [],
133
+ deletedIds: rawDiff.deletedIds ?? [],
134
+ deletedSelfSizes: rawDiff.deletedSelfSizes ?? [],
135
+ };
136
+ }
103
137
  #getCachedSnapshot(filePath) {
104
138
  const absolutePath = path.resolve(filePath);
105
139
  const cached = this.#snapshots.get(absolutePath);
@@ -108,16 +142,35 @@ export class HeapSnapshotManager {
108
142
  }
109
143
  return cached;
110
144
  }
145
+ async #getSortedRawClassDiffs(baseFilePath, currentFilePath) {
146
+ const baseSnapshot = await this.getSnapshot(baseFilePath);
147
+ const currentSnapshot = await this.getSnapshot(currentFilePath);
148
+ const interfaceDefinitions = await currentSnapshot.interfaceDefinitions();
149
+ const aggregatesForDiff = await baseSnapshot.aggregatesForDiff(interfaceDefinitions);
150
+ const baseSnapshotId = baseSnapshot.uid;
151
+ if (baseSnapshotId === undefined) {
152
+ throw new Error('Base snapshot UID is undefined');
153
+ }
154
+ // DevTools calculateSnapshotDiff uses the first parameter (baseSnapshotId)
155
+ // as a cache key. We pass the unique UID of the base snapshot.
156
+ const rawDiffs = await currentSnapshot.calculateSnapshotDiff(baseSnapshotId, aggregatesForDiff);
157
+ // Return a filtered and sorted array here to ensure that
158
+ // compare_heapsnapshot_summary and compare_heapsnapshot_details agree
159
+ // on indices.
160
+ return Object.values(rawDiffs)
161
+ .filter(diff => diff.addedCount > 0 || diff.removedCount > 0)
162
+ .sort((a, b) => b.sizeDelta - a.sizeDelta);
163
+ }
111
164
  async resolveClassKeyFromId(filePath, id) {
112
165
  const cached = this.#getCachedSnapshot(filePath);
113
166
  return cached.idToClassKey.get(id);
114
167
  }
115
- async #loadSnapshot(absolutePath) {
168
+ async #loadSnapshot(absolutePath, uid) {
116
169
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
117
170
  /* noop */
118
171
  }, import.meta.resolve('./third_party/devtools-heap-snapshot-worker.js'));
119
172
  const { promise: snapshotPromise, resolve: resolveSnapshot } = Promise.withResolvers();
120
- const loaderProxy = workerProxy.createLoader(1, snapshotProxy => {
173
+ const loaderProxy = workerProxy.createLoader(uid, snapshotProxy => {
121
174
  resolveSnapshot(snapshotProxy);
122
175
  });
123
176
  const fileStream = fsSync.createReadStream(absolutePath, {
@@ -131,6 +184,10 @@ export class HeapSnapshotManager {
131
184
  const snapshot = await snapshotPromise;
132
185
  return { snapshot, worker: workerProxy };
133
186
  }
187
+ async getDuplicateStrings(filePath) {
188
+ const snapshot = await this.getSnapshot(filePath);
189
+ return await snapshot.getDuplicateStrings();
190
+ }
134
191
  hasSnapshots() {
135
192
  return this.#snapshots.size > 0;
136
193
  }
@@ -16,7 +16,7 @@ import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
16
16
  import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
17
17
  import { listPages } from './tools/pages.js';
18
18
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
19
- import { ensureExtension, getTempFilePath, resolveCanonicalPath, } from './utils/files.js';
19
+ import { getTempFilePath, resolveCanonicalPath } from './utils/files.js';
20
20
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
21
21
  const DEFAULT_TIMEOUT = 5_000;
22
22
  const NAVIGATION_TIMEOUT = 10_000;
@@ -156,6 +156,13 @@ export class McpContext {
156
156
  throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
157
157
  }
158
158
  }
159
+ async ensureExtension(filePath, extension) {
160
+ const resolvedPath = path.resolve(filePath);
161
+ const currentExtension = path.extname(resolvedPath);
162
+ const outputPath = `${resolvedPath.slice(0, resolvedPath.length - currentExtension.length)}${extension}`;
163
+ await this.validatePath(outputPath);
164
+ return outputPath;
165
+ }
159
166
  resolveCdpRequestId(page, cdpRequestId) {
160
167
  if (!cdpRequestId) {
161
168
  this.logger?.('no network request');
@@ -223,12 +230,15 @@ export class McpContext {
223
230
  const currentSetting = page.emulationSettings;
224
231
  await this.emulate(currentSetting, page.pptrPage);
225
232
  }
233
+ get #hasNetworkBlockOrAllowlist() {
234
+ return !!(this.#options.allowList || this.#options.blocklist);
235
+ }
226
236
  async emulate(options, targetPage) {
227
237
  const page = targetPage ?? this.getSelectedPptrPage();
228
238
  const mcpPage = this.#getMcpPage(page);
229
239
  const newSettings = { ...mcpPage.emulationSettings };
230
240
  // Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
231
- if (this.#options.hasNetworkBlockOrAllowlist) {
241
+ if (this.#hasNetworkBlockOrAllowlist) {
232
242
  if (options.networkConditions !== undefined) {
233
243
  throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
234
244
  }
@@ -568,9 +578,8 @@ export class McpContext {
568
578
  return { filepath };
569
579
  }
570
580
  async saveFile(data, clientProvidedFilePath, extension) {
571
- await this.validatePath(clientProvidedFilePath);
581
+ const filePath = await this.ensureExtension(clientProvidedFilePath, extension);
572
582
  try {
573
- const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
574
583
  await fs.mkdir(path.dirname(filePath), { recursive: true });
575
584
  await fs.writeFile(filePath, data);
576
585
  return { filename: filePath };
@@ -646,6 +655,9 @@ export class McpContext {
646
655
  async getHeapSnapshotAggregates(filePath) {
647
656
  return await this.#heapSnapshotManager.getAggregates(filePath);
648
657
  }
658
+ async getHeapSnapshotDuplicateStrings(filePath) {
659
+ return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
660
+ }
649
661
  async getHeapSnapshotStats(filePath) {
650
662
  return await this.#heapSnapshotManager.getStats(filePath);
651
663
  }
@@ -670,12 +682,36 @@ export class McpContext {
670
682
  async getHeapSnapshotDominators(filePath, nodeId) {
671
683
  return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
672
684
  }
685
+ #validateUrlNotBlocked(url) {
686
+ if (!this.#options.blocklist) {
687
+ return;
688
+ }
689
+ for (const block of this.#options.blocklist) {
690
+ const pattern = new URLPattern(block);
691
+ if (pattern.test(url)) {
692
+ throw new Error(`Blocked by blocklist: ${url}`);
693
+ }
694
+ }
695
+ }
696
+ #validateUrlAllowed(url) {
697
+ if (!this.#options.allowList) {
698
+ return;
699
+ }
700
+ for (const allow of this.#options.allowList) {
701
+ const pattern = new URLPattern(allow);
702
+ if (pattern.test(url)) {
703
+ return;
704
+ }
705
+ }
706
+ throw new Error(`Not allowed by allowlist: ${url}`);
707
+ }
673
708
  async loadResource(path) {
674
709
  const url = new URL(path);
710
+ this.#validateUrlNotBlocked(url);
675
711
  switch (url.protocol) {
676
712
  case 'https:':
677
713
  case 'http:': {
678
- // TODO: Verify allow/block list
714
+ this.#validateUrlAllowed(url);
679
715
  const response = await fetch(url);
680
716
  if (!response.ok) {
681
717
  throw new Error(`Failed to load resource: ${url}`);
@@ -693,5 +729,11 @@ export class McpContext {
693
729
  async getHeapSnapshotEdges(filePath, nodeId) {
694
730
  return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
695
731
  }
732
+ async getHeapSnapshotClassDiffs(baseFilePath, currentFilePath) {
733
+ return await this.#heapSnapshotManager.getClassDiffs(baseFilePath, currentFilePath);
734
+ }
735
+ async getHeapSnapshotDetailedClassDiff(baseFilePath, currentFilePath, classIndex) {
736
+ return await this.#heapSnapshotManager.getDetailedClassDiff(baseFilePath, currentFilePath, classIndex);
737
+ }
696
738
  }
697
739
  //# sourceMappingURL=McpContext.js.map
@@ -4,14 +4,13 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
7
- import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
8
- import { isEdgeLike, isNodeLike } from './formatters/HeapSnapshotFormatter.js';
7
+ import { HeapSnapshotFormatter, isEdgeLike, isNodeLike, } from './formatters/HeapSnapshotFormatter.js';
9
8
  import { IssueFormatter } from './formatters/IssueFormatter.js';
10
9
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
11
10
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
12
11
  import { UncaughtError } from './PageCollector.js';
13
12
  import { TextSnapshot } from './TextSnapshot.js';
14
- import { DevTools, toonEncode } from './third_party/index.js';
13
+ import { DevTools, getToonEncode } from './third_party/index.js';
15
14
  import { handleDialog } from './tools/pages.js';
16
15
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
17
16
  import { paginate } from './utils/pagination.js';
@@ -334,6 +333,14 @@ export class McpResponse {
334
333
  pagination: options,
335
334
  };
336
335
  }
336
+ setHeapSnapshotDuplicateStrings(duplicateStrings, options) {
337
+ this.#heapSnapshotOptions = {
338
+ ...this.#heapSnapshotOptions,
339
+ include: true,
340
+ duplicateStrings,
341
+ pagination: options,
342
+ };
343
+ }
337
344
  setHeapSnapshotRetainingPaths(retainingPaths) {
338
345
  this.#heapSnapshotOptions = {
339
346
  ...this.#heapSnapshotOptions,
@@ -348,6 +355,20 @@ export class McpResponse {
348
355
  dominators,
349
356
  };
350
357
  }
358
+ setHeapSnapshotClassDiffs(classDiffs) {
359
+ this.#heapSnapshotOptions = {
360
+ ...this.#heapSnapshotOptions,
361
+ include: true,
362
+ classDiffs,
363
+ };
364
+ }
365
+ setHeapSnapshotDetailedClassDiff(detailedClassDiff) {
366
+ this.#heapSnapshotOptions = {
367
+ ...this.#heapSnapshotOptions,
368
+ include: true,
369
+ detailedClassDiff,
370
+ };
371
+ }
351
372
  attachImage(value) {
352
373
  this.#images.push(value);
353
374
  }
@@ -548,6 +569,18 @@ export class McpResponse {
548
569
  }
549
570
  async format(toolName, context, data, useToon) {
550
571
  const structuredContent = {};
572
+ let toonEncode;
573
+ if (useToon) {
574
+ try {
575
+ toonEncode = await getToonEncode();
576
+ }
577
+ catch {
578
+ throw new Error('The `@toon-format/toon` package is required to use the experimental TOON format. ' +
579
+ 'Make sure the peer dependency is installed:\n' +
580
+ '- For npx: npx --package chrome-devtools-mcp@latest --package @toon-format/toon@latest chrome-devtools-mcp --experimentalToonFormat\n' +
581
+ '- For npm: npm install @toon-format/toon (add -g if installed globally)');
582
+ }
583
+ }
551
584
  const response = [];
552
585
  if (this.#textResponseLines.length) {
553
586
  structuredContent.message = this.#textResponseLines.join('\n');
@@ -726,7 +759,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
726
759
  else {
727
760
  structuredContent.snapshot = data.snapshot.toJSON();
728
761
  response.push('## Latest page snapshot');
729
- response.push(useToon
762
+ response.push(useToon && toonEncode
730
763
  ? toonEncode(structuredContent.snapshot)
731
764
  : data.snapshot.toString());
732
765
  }
@@ -754,7 +787,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
754
787
  const paginatedRecord = Object.fromEntries(paginationData.items);
755
788
  const formatter = new HeapSnapshotFormatter(paginatedRecord);
756
789
  structuredContent.heapSnapshotData = formatter.toJSON();
757
- response.push(useToon
790
+ response.push(useToon && toonEncode
758
791
  ? toonEncode(structuredContent.heapSnapshotData)
759
792
  : formatter.toString());
760
793
  }
@@ -808,6 +841,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
808
841
  }
809
842
  structuredContent.heapSnapshotDominators = dominators;
810
843
  }
844
+ const classDiffs = this.#heapSnapshotOptions.classDiffs;
845
+ if (classDiffs) {
846
+ response.push('### Heap Snapshot Diff');
847
+ response.push(useToon && toonEncode
848
+ ? toonEncode(classDiffs)
849
+ : HeapSnapshotFormatter.formatDiffSummary(classDiffs));
850
+ structuredContent.heapSnapshotClassDiffs = classDiffs;
851
+ }
852
+ const detailedClassDiff = this.#heapSnapshotOptions.detailedClassDiff;
853
+ if (detailedClassDiff) {
854
+ response.push('### Heap Snapshot Detailed Diff');
855
+ response.push(useToon && toonEncode
856
+ ? toonEncode(detailedClassDiff)
857
+ : HeapSnapshotFormatter.formatDiffDetails(detailedClassDiff));
858
+ structuredContent.heapSnapshotDetailedClassDiff = detailedClassDiff;
859
+ }
860
+ const duplicateStrings = this.#heapSnapshotOptions.duplicateStrings;
861
+ if (duplicateStrings) {
862
+ response.push('### Duplicate Strings');
863
+ const paginationData = this.#dataWithPagination(duplicateStrings, this.#heapSnapshotOptions.pagination);
864
+ structuredContent.pagination = paginationData.pagination;
865
+ response.push(...paginationData.info);
866
+ const formatted = HeapSnapshotFormatter.formatDuplicateStrings(paginationData.items);
867
+ response.push(formatted);
868
+ structuredContent.heapSnapshotDuplicateStrings = paginationData.items;
869
+ }
811
870
  }
812
871
  if (data.detailedNetworkRequest) {
813
872
  response.push(data.detailedNetworkRequest.toStringDetailed());
@@ -879,7 +938,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
879
938
  response.push(...paginationData.info);
880
939
  if (data.networkRequests) {
881
940
  structuredContent.networkRequests = paginationData.items.map(i => i.toJSON());
882
- response.push(...(useToon
941
+ response.push(...(useToon && toonEncode
883
942
  ? [toonEncode(structuredContent.networkRequests)]
884
943
  : paginationData.items.map(i => i.toString())));
885
944
  }
@@ -897,7 +956,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
897
956
  structuredContent.pagination = paginationData.pagination;
898
957
  structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON());
899
958
  response.push(...paginationData.info);
900
- if (useToon) {
959
+ if (useToon && toonEncode) {
901
960
  response.push(toonEncode(structuredContent.consoleMessages));
902
961
  }
903
962
  else {
@@ -82,6 +82,30 @@ export const commands = {
82
82
  },
83
83
  },
84
84
  },
85
+ compare_heapsnapshots: {
86
+ description: 'Loads two memory heapsnapshots and returns the comparison. If classIndex is provided, returns detailed diff for that class, otherwise returns summary diff. (requires flag: --memoryDebugging=true)',
87
+ category: 'Memory',
88
+ args: {
89
+ baseFilePath: {
90
+ name: 'baseFilePath',
91
+ type: 'string',
92
+ description: 'A path to the base .heapsnapshot file (earlier snapshot).',
93
+ required: true,
94
+ },
95
+ currentFilePath: {
96
+ name: 'currentFilePath',
97
+ type: 'string',
98
+ description: 'A path to the current .heapsnapshot file (later snapshot).',
99
+ required: true,
100
+ },
101
+ classIndex: {
102
+ name: 'classIndex',
103
+ type: 'number',
104
+ description: 'Optional 0-based index of the class in the summary list to filter results, showing individual objects.',
105
+ required: false,
106
+ },
107
+ },
108
+ },
85
109
  drag: {
86
110
  description: 'Drag an element onto another element',
87
111
  category: 'Input automation',
@@ -157,13 +181,13 @@ export const commands = {
157
181
  },
158
182
  },
159
183
  evaluate_script: {
160
- description: 'Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,\nso returned values have to be JSON-serializable.',
184
+ description: 'Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON, so returned values have to be JSON-serializable.',
161
185
  category: 'Debugging',
162
186
  args: {
163
187
  function: {
164
188
  name: 'function',
165
189
  type: 'string',
166
- description: 'A JavaScript function declaration to be executed by the tool in the currently selected page.\nExample without arguments: `() => {\n return document.title\n}` or `async () => {\n return await fetch("example.com")\n}`.\nExample with arguments: `(el) => {\n return el.innerText;\n}`\n',
190
+ description: 'A JavaScript function declaration to be executed by the tool in the currently selected page.\nExample without arguments: `() => document.title` or `async () => await fetch("example.com")`.\nExample with arguments: `(el) => el.innerText`\n',
167
191
  required: true,
168
192
  },
169
193
  args: {
@@ -330,6 +354,30 @@ export const commands = {
330
354
  },
331
355
  },
332
356
  },
357
+ get_heapsnapshot_duplicate_strings: {
358
+ description: 'Loads a memory heapsnapshot and returns duplicate strings grouped by their value. (requires flag: --memoryDebugging=true)',
359
+ category: 'Memory',
360
+ args: {
361
+ filePath: {
362
+ name: 'filePath',
363
+ type: 'string',
364
+ description: 'A path to a .heapsnapshot file to read.',
365
+ required: true,
366
+ },
367
+ pageIdx: {
368
+ name: 'pageIdx',
369
+ type: 'number',
370
+ description: 'The page index for pagination.',
371
+ required: false,
372
+ },
373
+ pageSize: {
374
+ name: 'pageSize',
375
+ type: 'number',
376
+ description: 'The page size for pagination.',
377
+ required: false,
378
+ },
379
+ },
380
+ },
333
381
  get_heapsnapshot_edges: {
334
382
  description: 'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)',
335
383
  category: 'Memory',
@@ -540,7 +588,7 @@ export const commands = {
540
588
  },
541
589
  },
542
590
  list_3p_developer_tools: {
543
- description: "Lists all third-party developer tools the page exposes for providing runtime information.\n Third-party developer tools can be called via the 'execute_3p_developer_tool()' MCP tool.\n Alternatively, third-party developer tools can be executed by calling 'evaluate_script' and adding the\n following command to the script:\n 'window.__dtmcp.executeTool(toolName, params)'\n This might be helpful when the third-party developer tools return non-serializable values or when composing\n third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true)",
591
+ description: "Lists all third-party developer tools the page exposes for providing runtime information.\nThird-party developer tools can be called via the 'execute_3p_developer_tool()' MCP tool.\nAlternatively, third-party developer tools can be executed by calling 'evaluate_script' and adding the\nfollowing command to the script:\n`window.__dtmcp.executeTool(toolName, params)`\nThis might be helpful when the third-party developer tools return non-serializable values or when composing\nthird-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true)",
544
592
  category: 'Third-party',
545
593
  args: {},
546
594
  },
@@ -157,7 +157,7 @@ export const cliOptions = {
157
157
  },
158
158
  experimentalToonFormat: {
159
159
  type: 'boolean',
160
- describe: 'Whether to format structured data in text response using Token-Oriented Object Notation. Defaults to false which represents the embedded content as formatted JSON instead.',
160
+ describe: 'Whether to format structured data in text response using Token-Oriented Object Notation (requires @toon-format/toon). If running via npx, use: npx --package chrome-devtools-mcp@latest --package @toon-format/toon@latest chrome-devtools-mcp --experimentalToonFormat',
161
161
  hidden: true,
162
162
  },
163
163
  experimentalIncludeAllPages: {
@@ -41,6 +41,7 @@ startCliOptions.isolated.description =
41
41
  'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.';
42
42
  startCliOptions.categoryExtensions.default = true;
43
43
  const y = yargs(hideBin(process.argv))
44
+ .locale('en') // Force English to ensure error string matching works in .fail, all custom messages we output are in English anyways
44
45
  .scriptName('chrome-devtools')
45
46
  .showHelpOnFail(true)
46
47
  .usage('chrome-devtools <command> [...args] --flags')
@@ -55,7 +56,30 @@ const y = yargs(hideBin(process.argv))
55
56
  .version(VERSION)
56
57
  .strict()
57
58
  .help(true)
58
- .wrap(120);
59
+ .wrap(120)
60
+ .fail((msg, err) => {
61
+ if (msg) {
62
+ console.error('Error:', msg);
63
+ if (msg.includes('Not enough non-option arguments') ||
64
+ msg.includes('Unknown argument') ||
65
+ msg.includes('Unknown arguments')) {
66
+ console.error('\n=========================================');
67
+ console.error('💡 TIP FOR AI AGENT / DEVELOPER:');
68
+ console.error('In the `chrome-devtools` CLI:');
69
+ console.error('1. Required parameters MUST be passed as positional arguments (without flags).');
70
+ console.error(' - INCORRECT: chrome-devtools evaluate_script --expression "() => document.title"');
71
+ console.error(' - CORRECT: chrome-devtools evaluate_script "() => document.title"');
72
+ console.error('2. Optional parameters are passed as double-dash options/flags (e.g. --pageId 1).');
73
+ console.error('3. Make sure to escape quotes properly for your shell environment.');
74
+ console.error('Run `chrome-devtools <command> --help` to see exact positional and optional parameters.');
75
+ console.error('=========================================');
76
+ }
77
+ }
78
+ else if (err) {
79
+ console.error(err);
80
+ }
81
+ process.exit(1);
82
+ });
59
83
  y.command('start', 'Start or restart chrome-devtools-mcp', y => y
60
84
  .options(startCliOptions)
61
85
  .example('$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser')
@@ -23,7 +23,7 @@ const pidFilePath = getPidFilePath(sessionId);
23
23
  const pidDir = path.dirname(pidFilePath);
24
24
  const currentUserUid = os.userInfo().uid;
25
25
  try {
26
- fs.mkdirSync(pidDir, { recursive: true });
26
+ fs.mkdirSync(pidDir, { recursive: true, mode: 0o700 });
27
27
  if (os.platform() !== 'win32') {
28
28
  // POSIX specific checks
29
29
  try {
@@ -67,6 +67,16 @@ export class HeapSnapshotFormatter {
67
67
  }
68
68
  return lines.join('\n');
69
69
  }
70
+ static formatDuplicateStrings(groups) {
71
+ const lines = [];
72
+ lines.push('value,count,totalSelfSize,totalRetainedSize,truncated,nodeIds');
73
+ for (const group of groups) {
74
+ const nodeIds = group.nodes.map(n => `@${n.id}`).join(' ');
75
+ const truncated = group.truncated ?? false;
76
+ lines.push(`${JSON.stringify(group.value)},${group.count},${DevTools.I18n.ByteUtilities.formatBytesToKb(group.totalSelfSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(group.totalRetainedSize)},${truncated},${nodeIds}`);
77
+ }
78
+ return lines.join('\n');
79
+ }
70
80
  #getSortedAggregates() {
71
81
  return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
72
82
  }
@@ -93,5 +103,38 @@ export class HeapSnapshotFormatter {
93
103
  static sort(aggregates) {
94
104
  return Object.entries(aggregates).sort((a, b) => b[1].maxRet - a[1].maxRet);
95
105
  }
106
+ static formatDiffSummary(diffs) {
107
+ const lines = [];
108
+ lines.push('index,className,addedCount,removedCount,countDelta,addedSize,removedSize,sizeDelta');
109
+ let index = 0;
110
+ for (const diff of diffs) {
111
+ lines.push(`${index},${diff.className},${diff.addedCount},${diff.removedCount},${diff.countDelta},${DevTools.I18n.ByteUtilities.formatBytesToKb(diff.addedSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(diff.removedSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(diff.sizeDelta)}`);
112
+ index++;
113
+ }
114
+ return lines.join('\n');
115
+ }
116
+ static formatDiffDetails(diff) {
117
+ const lines = [];
118
+ lines.push(`${diff.className}: # new: ${diff.addedCount}, # deleted: ${diff.removedCount}, # delta: ${formatSignedCount(diff.countDelta)}, alloc size: ${formatSignedSize(diff.addedSize)}, freed size: ${formatSignedSize(diff.removedSize)}, size delta: ${formatSignedSize(diff.sizeDelta)}`);
119
+ const addedIds = diff.addedIds;
120
+ const addedSelfSizes = diff.addedSelfSizes;
121
+ const deletedIds = diff.deletedIds;
122
+ const deletedSelfSizes = diff.deletedSelfSizes;
123
+ lines.push(`Objects:`);
124
+ for (let i = 0; i < addedIds.length; i++) {
125
+ lines.push(` + @${addedIds[i]} (self_size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(addedSelfSizes[i])})`);
126
+ }
127
+ for (let i = 0; i < deletedIds.length; i++) {
128
+ lines.push(` - @${deletedIds[i]} (self_size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(deletedSelfSizes[i])})`);
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+ }
133
+ function formatSignedCount(n) {
134
+ return n > 0 ? `+${n}` : `${n}`;
135
+ }
136
+ function formatSignedSize(bytes) {
137
+ const formatted = DevTools.I18n.ByteUtilities.formatBytesToKb(bytes);
138
+ return bytes > 0 ? `+${formatted}` : formatted;
96
139
  }
97
140
  //# sourceMappingURL=HeapSnapshotFormatter.js.map
@@ -108,8 +108,8 @@ export async function createMcpServer(serverArgs, options) {
108
108
  experimentalDevToolsDebugging: devtools,
109
109
  experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
110
110
  performanceCrux: serverArgs.performanceCrux,
111
- hasNetworkBlockOrAllowlist: Boolean((blocklist && blocklist.length > 0) ||
112
- (allowlist && allowlist.length > 0)),
111
+ allowList: allowlist,
112
+ blocklist: blocklist,
113
113
  });
114
114
  await updateRoots();
115
115
  }
@@ -16,11 +16,13 @@ export async function loadIssueDescriptions() {
16
16
  }
17
17
  const files = await fs.promises.readdir(DESCRIPTIONS_PATH);
18
18
  const descriptions = {};
19
- for (const file of files) {
20
- if (!file.endsWith('.md')) {
21
- continue;
22
- }
19
+ const results = await Promise.all(files
20
+ .filter(file => file.endsWith('.md'))
21
+ .map(async (file) => {
23
22
  const content = await fs.promises.readFile(path.join(DESCRIPTIONS_PATH, file), 'utf-8');
23
+ return { file, content };
24
+ }));
25
+ for (const { file, content } of results) {
24
26
  descriptions[file] = content;
25
27
  }
26
28
  issueDescriptions = descriptions;
@@ -867,7 +867,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
867
867
 
868
868
  Name: puppeteer-core
869
869
  URL: https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core
870
- Version: 25.2.0
870
+ Version: 25.2.1
871
871
  License: Apache-2.0
872
872
 
873
873
  -------------------- DEPENDENCY DIVIDER --------------------
@@ -906,36 +906,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
906
906
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
907
907
 
908
908
 
909
- -------------------- DEPENDENCY DIVIDER --------------------
910
-
911
- Name: @toon-format/toon
912
- URL: https://toonformat.dev
913
- Version: 2.3.0
914
- License: MIT
915
-
916
- MIT License
917
-
918
- Copyright (c) 2025-PRESENT Johann Schopplich
919
-
920
- Permission is hereby granted, free of charge, to any person obtaining a copy
921
- of this software and associated documentation files (the "Software"), to deal
922
- in the Software without restriction, including without limitation the rights
923
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
924
- copies of the Software, and to permit persons to whom the Software is
925
- furnished to do so, subject to the following conditions:
926
-
927
- The above copyright notice and this permission notice shall be included in all
928
- copies or substantial portions of the Software.
929
-
930
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
931
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
932
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
933
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
934
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
935
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
936
- SOFTWARE.
937
-
938
-
939
909
  -------------------- DEPENDENCY DIVIDER --------------------
940
910
 
941
911
  Name: chrome-devtools-frontend