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 +5 -3
- package/build/src/HeapSnapshotManager.js +60 -3
- package/build/src/McpContext.js +47 -5
- package/build/src/McpResponse.js +66 -7
- package/build/src/bin/chrome-devtools-cli-options.js +51 -3
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +1 -1
- package/build/src/bin/chrome-devtools.js +25 -1
- package/build/src/daemon/daemon.js +1 -1
- package/build/src/formatters/HeapSnapshotFormatter.js +43 -0
- package/build/src/index.js +2 -2
- package/build/src/issue-descriptions.js +6 -4
- package/build/src/third_party/THIRD_PARTY_NOTICES +1 -31
- package/build/src/third_party/bundled-packages.json +2 -3
- package/build/src/third_party/devtools-heap-snapshot-worker.js +81 -0
- package/build/src/third_party/index.js +894 -686
- package/build/src/tools/memory.js +60 -4
- package/build/src/tools/screencast.js +1 -2
- package/build/src/tools/script.js +3 -10
- package/build/src/tools/thirdPartyDeveloper.js +6 -6
- package/build/src/utils/files.js +0 -4
- package/build/src/version.js +1 -1
- package/package.json +11 -3
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** (
|
|
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
|
|
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(
|
|
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
|
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -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 {
|
|
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.#
|
|
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.
|
|
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
|
-
|
|
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
|
package/build/src/McpResponse.js
CHANGED
|
@@ -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,
|
|
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
|
|
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: `() =>
|
|
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.\
|
|
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.
|
|
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
|
package/build/src/index.js
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
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
|