chrome-devtools-mcp 1.3.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.
Files changed (40) hide show
  1. package/README.md +42 -5
  2. package/build/src/HeapSnapshotManager.js +60 -3
  3. package/build/src/McpContext.js +47 -5
  4. package/build/src/McpResponse.js +66 -7
  5. package/build/src/bin/check-latest-version.js +1 -0
  6. package/build/src/bin/chrome-devtools-cli-options.js +51 -3
  7. package/build/src/bin/chrome-devtools-mcp-cli-options.js +3 -3
  8. package/build/src/bin/chrome-devtools.js +25 -1
  9. package/build/src/daemon/daemon.js +1 -1
  10. package/build/src/formatters/HeapSnapshotFormatter.js +43 -0
  11. package/build/src/formatters/NetworkFormatter.js +3 -1
  12. package/build/src/index.js +2 -2
  13. package/build/src/issue-descriptions.js +6 -4
  14. package/build/src/third_party/THIRD_PARTY_NOTICES +3 -33
  15. package/build/src/third_party/bundled-packages.json +3 -4
  16. package/build/src/third_party/devtools-heap-snapshot-worker.js +94 -0
  17. package/build/src/third_party/index.js +2396 -896
  18. package/build/src/tools/memory.js +60 -4
  19. package/build/src/tools/screencast.js +1 -2
  20. package/build/src/tools/script.js +3 -10
  21. package/build/src/tools/thirdPartyDeveloper.js +6 -6
  22. package/build/src/utils/check-for-updates.js +1 -0
  23. package/build/src/utils/files.js +0 -4
  24. package/build/src/version.js +1 -1
  25. package/package.json +15 -6
  26. package/skills/a11y-debugging/SKILL.md +89 -0
  27. package/skills/a11y-debugging/references/a11y-snippets.md +92 -0
  28. package/skills/chrome-devtools/SKILL.md +72 -0
  29. package/skills/chrome-devtools-cli/SKILL.md +153 -0
  30. package/skills/chrome-devtools-cli/references/installation.md +14 -0
  31. package/skills/debug-optimize-lcp/SKILL.md +121 -0
  32. package/skills/debug-optimize-lcp/references/elements-and-size.md +27 -0
  33. package/skills/debug-optimize-lcp/references/lcp-breakdown.md +23 -0
  34. package/skills/debug-optimize-lcp/references/lcp-snippets.md +79 -0
  35. package/skills/debug-optimize-lcp/references/optimization-strategies.md +38 -0
  36. package/skills/memory-leak-debugging/SKILL.md +50 -0
  37. package/skills/memory-leak-debugging/references/common-leaks.md +33 -0
  38. package/skills/memory-leak-debugging/references/compare_snapshots.js +109 -0
  39. package/skills/memory-leak-debugging/references/memlab.md +29 -0
  40. package/skills/troubleshooting/SKILL.md +98 -0
@@ -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: {
@@ -193,12 +193,12 @@ export const cliOptions = {
193
193
  },
194
194
  blockedUrlPattern: {
195
195
  type: 'array',
196
- describe: 'Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
196
+ describe: "Restricts browser's network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.",
197
197
  conflicts: ['allowedUrlPattern'],
198
198
  },
199
199
  allowedUrlPattern: {
200
200
  type: 'array',
201
- describe: 'Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
201
+ describe: "Restricts browser's network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.",
202
202
  conflicts: ['blockedUrlPattern'],
203
203
  },
204
204
  ignoreDefaultChromeArg: {
@@ -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
@@ -226,7 +226,9 @@ function converNetworkRequestDetailedToStringDetailed(data) {
226
226
  if (redirectChain?.length) {
227
227
  response.push(`### Redirect chain`);
228
228
  let indent = 0;
229
- for (const request of redirectChain.reverse()) {
229
+ // `redirectChain` is already ordered by toJSONDetailed(); don't reverse it
230
+ // again here or the text output contradicts structuredContent (the JSON).
231
+ for (const request of redirectChain) {
230
232
  response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`);
231
233
  indent++;
232
234
  }
@@ -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;
@@ -324,7 +324,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
324
324
 
325
325
  Name: semver
326
326
  URL: git+https://github.com/npm/node-semver.git
327
- Version: 7.8.3
327
+ Version: 7.8.5
328
328
  License: ISC
329
329
 
330
330
  The ISC License
@@ -867,14 +867,14 @@ 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.1.0
870
+ Version: 25.2.1
871
871
  License: Apache-2.0
872
872
 
873
873
  -------------------- DEPENDENCY DIVIDER --------------------
874
874
 
875
875
  Name: @puppeteer/browsers
876
876
  URL: https://github.com/puppeteer/puppeteer/tree/main/packages/browsers
877
- Version: 3.0.4
877
+ Version: 3.0.5
878
878
  License: Apache-2.0
879
879
 
880
880
  -------------------- 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
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "@modelcontextprotocol/sdk": "1.29.0",
3
- "@toon-format/toon": "^2.2.0",
4
- "chrome-devtools-frontend": "1.0.1646286",
3
+ "chrome-devtools-frontend": "1.0.1652307",
5
4
  "core-js": "3.49.0",
6
5
  "debug": "4.4.3",
7
- "lighthouse": "13.3.0",
6
+ "lighthouse": "13.4.0",
8
7
  "semver": "^7.7.4",
9
8
  "urlpattern-polyfill": "^10.1.0",
10
9
  "yargs": "18.0.0",
11
- "puppeteer-core": "25.1.0"
10
+ "puppeteer-core": "25.2.1"
12
11
  }
@@ -5930,6 +5930,9 @@ class HeapSnapshotProxy extends HeapSnapshotProxyObject {
5930
5930
  aggregatesWithFilter(filter) {
5931
5931
  return this.callMethodPromise('aggregatesWithFilter', filter);
5932
5932
  }
5933
+ getDuplicateStrings() {
5934
+ return this.callMethodPromise('getDuplicateStrings');
5935
+ }
5933
5936
  aggregatesForDiff(interfaceDefinitions) {
5934
5937
  return this.callMethodPromise('aggregatesForDiff', interfaceDefinitions);
5935
5938
  }
@@ -6712,6 +6715,45 @@ class HeapSnapshotNode {
6712
6715
  value |= detachedness;
6713
6716
  this.#setDetachednessAndClassIndex(value);
6714
6717
  }
6718
+ findInternalEdgeTarget(name) {
6719
+ for (let iter = this.edges(); iter.hasNext(); iter.next()) {
6720
+ const edge = iter.edge;
6721
+ if (!edge.isInternal()) {
6722
+ continue;
6723
+ }
6724
+ if (edge.name() === name) {
6725
+ return edge.node();
6726
+ }
6727
+ }
6728
+ return undefined;
6729
+ }
6730
+ nodeValueAsBool() {
6731
+ if (this.rawType() !== this.snapshot.nodeNumberType) {
6732
+ return undefined;
6733
+ }
6734
+ if (this.rawName() !== 'bool') {
6735
+ return undefined;
6736
+ }
6737
+ const valNode = this.findInternalEdgeTarget('value');
6738
+ if (!valNode) {
6739
+ return undefined;
6740
+ }
6741
+ const rawName = valNode.rawName();
6742
+ if (rawName === 'true') {
6743
+ return true;
6744
+ }
6745
+ if (rawName === 'false') {
6746
+ return false;
6747
+ }
6748
+ return undefined;
6749
+ }
6750
+ nodeIsTruncatedString() {
6751
+ const truncNode = this.findInternalEdgeTarget('truncated');
6752
+ if (!truncNode) {
6753
+ return false;
6754
+ }
6755
+ return truncNode.nodeValueAsBool() === true;
6756
+ }
6715
6757
  }
6716
6758
  class HeapSnapshotNodeIterator {
6717
6759
  node;
@@ -6927,6 +6969,7 @@ class HeapSnapshot {
6927
6969
  nodeSyntheticType;
6928
6970
  nodeClosureType;
6929
6971
  nodeRegExpType;
6972
+ nodeNumberType;
6930
6973
  edgeFieldsCount;
6931
6974
  edgeTypeOffset;
6932
6975
  edgeNameOffset;
@@ -7001,6 +7044,7 @@ class HeapSnapshot {
7001
7044
  this.nodeSyntheticType = this.nodeTypes.indexOf('synthetic');
7002
7045
  this.nodeClosureType = this.nodeTypes.indexOf('closure');
7003
7046
  this.nodeRegExpType = this.nodeTypes.indexOf('regexp');
7047
+ this.nodeNumberType = this.nodeTypes.indexOf('number');
7004
7048
  this.edgeFieldsCount = meta.edge_fields.length;
7005
7049
  this.edgeTypeOffset = meta.edge_fields.indexOf('type');
7006
7050
  this.edgeNameOffset = meta.edge_fields.indexOf('name_or_index');
@@ -7280,6 +7324,43 @@ class HeapSnapshot {
7280
7324
  const key = filter ? filter.key : 'allObjects';
7281
7325
  return this.getAggregatesByClassKey(false, key, filter);
7282
7326
  }
7327
+ getDuplicateStrings() {
7328
+ const filter = this.createNamedFilter('duplicatedStrings');
7329
+ const groupsMap = new Map();
7330
+ const node = this.createNode(0);
7331
+ for (let i = 0; i < this.nodeCount; ++i) {
7332
+ node.nodeIndex = i * this.nodeFieldCount;
7333
+ if (filter(node)) {
7334
+ const name = node.name();
7335
+ const truncated = node.nodeIsTruncatedString();
7336
+ let group = groupsMap.get(name);
7337
+ if (!group) {
7338
+ group = {
7339
+ value: name,
7340
+ count: 0,
7341
+ totalSelfSize: 0,
7342
+ totalRetainedSize: 0,
7343
+ nodes: [],
7344
+ truncated,
7345
+ };
7346
+ groupsMap.set(name, group);
7347
+ }
7348
+ else if (truncated) {
7349
+ group.truncated = true;
7350
+ }
7351
+ group.count++;
7352
+ group.totalSelfSize += node.selfSize();
7353
+ group.totalRetainedSize += node.retainedSize();
7354
+ group.nodes.push({
7355
+ id: node.id(),
7356
+ selfSize: node.selfSize(),
7357
+ retainedSize: node.retainedSize(),
7358
+ distance: node.distance(),
7359
+ });
7360
+ }
7361
+ }
7362
+ return Array.from(groupsMap.values()).sort((a, b) => b.totalRetainedSize - a.totalRetainedSize);
7363
+ }
7283
7364
  createNodeIdFilter(minNodeId, maxNodeId) {
7284
7365
  function nodeIdFilter(node) {
7285
7366
  const id = node.id();
@@ -7334,6 +7415,12 @@ class HeapSnapshot {
7334
7415
  }
7335
7416
  };
7336
7417
  switch (filterName) {
7418
+ case 'objectsRetainedByContexts':
7419
+ traverse((_node, edge) => {
7420
+ return !this.isContextObject(edge.node());
7421
+ });
7422
+ markUnreachableNodes();
7423
+ return (node) => !getBit(node);
7337
7424
  case 'objectsRetainedByDetachedDomNodes':
7338
7425
  traverse((_node, edge) => {
7339
7426
  return edge.node().detachedness() !== 2 ;
@@ -7477,6 +7564,9 @@ class HeapSnapshot {
7477
7564
  isUserRoot(_node) {
7478
7565
  return true;
7479
7566
  }
7567
+ isContextObject(_node) {
7568
+ return false;
7569
+ }
7480
7570
  calculateShallowSizes() {
7481
7571
  }
7482
7572
  calculateDistances(isForRetainersView, filter) {
@@ -9011,6 +9101,10 @@ class JSHeapSnapshot extends HeapSnapshot {
9011
9101
  isUserRoot(node) {
9012
9102
  return node.isUserRoot() || node.isDocumentDOMTreesRoot();
9013
9103
  }
9104
+ isContextObject(node) {
9105
+ const name = node.rawName();
9106
+ return name === 'system / Context' || name.startsWith('system / Context / ');
9107
+ }
9014
9108
  userObjectsMapAndFlag() {
9015
9109
  return { map: this.flags, flag: this.nodeFlags.pageObject };
9016
9110
  }