chrome-devtools-mcp 1.0.1 → 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.
- package/README.md +7 -7
- package/build/src/DevtoolsUtils.js +1 -1
- package/build/src/HeapSnapshotManager.js +16 -16
- package/build/src/McpContext.js +58 -23
- package/build/src/McpResponse.js +1 -1
- package/build/src/bin/chrome-devtools-cli-options.js +55 -49
- package/build/src/bin/chrome-devtools-mcp-main.js +38 -0
- package/build/src/browser.js +36 -2
- package/build/src/daemon/daemon.js +62 -5
- package/build/src/formatters/HeapSnapshotFormatter.js +7 -7
- package/build/src/third_party/THIRD_PARTY_NOTICES +4 -4
- package/build/src/third_party/bundled-packages.json +2 -2
- package/build/src/third_party/devtools-formatter-worker.js +2 -0
- package/build/src/third_party/devtools-heap-snapshot-worker.js +2 -0
- package/build/src/third_party/index.js +197 -75
- package/build/src/tools/ToolDefinition.js +1 -1
- package/build/src/tools/emulation.js +25 -0
- package/build/src/tools/extensions.js +2 -0
- package/build/src/tools/input.js +1 -1
- package/build/src/tools/lighthouse.js +1 -1
- package/build/src/tools/memory.js +19 -21
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/performance.js +9 -6
- package/build/src/tools/screencast.js +1 -1
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +1 -1
- package/build/src/tools/snapshot.js +1 -1
- package/build/src/trace-processing/parse.js +2 -2
- package/build/src/utils/files.js +43 -0
- package/build/src/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.org/package/chrome-devtools-mcp)
|
|
4
4
|
|
|
5
|
-
Chrome DevTools for agents (`chrome-devtools-mcp`) lets your coding agent (such as
|
|
5
|
+
Chrome DevTools for agents (`chrome-devtools-mcp`) lets your coding agent (such as Antigravity, Claude, Cursor or Copilot)
|
|
6
6
|
control and inspect a live Chrome browser. It acts as a Model-Context-Protocol
|
|
7
7
|
(MCP) server, giving your AI coding assistant access to the full power of
|
|
8
8
|
Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.
|
|
@@ -249,7 +249,7 @@ and the expert guidance it needs to use them effectively.
|
|
|
249
249
|
|
|
250
250
|
1. Open the **Command Palette** (`Cmd+Shift+P` on macOS or `Ctrl+Shift+P` on Windows/Linux).
|
|
251
251
|
2. Search for and run the **Chat: Install Plugin From Source** command.
|
|
252
|
-
3. Paste in our repository
|
|
252
|
+
3. Paste in our repository name: `ChromeDevTools/chrome-devtools-mcp`.
|
|
253
253
|
|
|
254
254
|
That's it! Your agent is now supercharged with Chrome DevTools capabilities.
|
|
255
255
|
|
|
@@ -515,11 +515,11 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
515
515
|
- [`screencast_start`](docs/tool-reference.md#screencast_start)
|
|
516
516
|
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
|
|
517
517
|
- **Memory** (5 tools)
|
|
518
|
-
- [`
|
|
519
|
-
- [`
|
|
520
|
-
- [`
|
|
521
|
-
- [`
|
|
522
|
-
- [`
|
|
518
|
+
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
|
|
519
|
+
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
|
|
520
|
+
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
|
|
521
|
+
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
|
|
522
|
+
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
|
|
523
523
|
- **Extensions** (5 tools)
|
|
524
524
|
- [`install_extension`](docs/tool-reference.md#install_extension)
|
|
525
525
|
- [`list_extensions`](docs/tool-reference.md#list_extensions)
|
|
@@ -109,7 +109,7 @@ const DEFAULT_FACTORY = async (page) => {
|
|
|
109
109
|
targetManager.observeModels(DevTools.NetworkManager.NetworkManager, DISABLE_NETWORK);
|
|
110
110
|
const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
111
111
|
/* parentTarget */ null, session.id(), undefined, connection);
|
|
112
|
-
return { target, universe };
|
|
112
|
+
return { target, universe, session };
|
|
113
113
|
};
|
|
114
114
|
// We don't want to pause any DevTools universe session ever on the MCP side.
|
|
115
115
|
//
|
|
@@ -20,8 +20,8 @@ export class HeapSnapshotManager {
|
|
|
20
20
|
this.#snapshots.set(absolutePath, {
|
|
21
21
|
snapshot,
|
|
22
22
|
worker,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
idToClassKey: new Map(),
|
|
24
|
+
classKeyToId: new Map(),
|
|
25
25
|
idGenerator: createIdGenerator(),
|
|
26
26
|
});
|
|
27
27
|
return snapshot;
|
|
@@ -31,10 +31,10 @@ export class HeapSnapshotManager {
|
|
|
31
31
|
const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
|
|
32
32
|
const aggregates = await snapshot.aggregatesWithFilter(filter);
|
|
33
33
|
for (const key of Object.keys(aggregates)) {
|
|
34
|
-
const
|
|
34
|
+
const id = await this.getOrCreateIdForClassKey(filePath, key);
|
|
35
35
|
const aggregate = aggregates[key];
|
|
36
36
|
if (aggregate) {
|
|
37
|
-
aggregate[stableIdSymbol] =
|
|
37
|
+
aggregate[stableIdSymbol] = id;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
return aggregates;
|
|
@@ -47,22 +47,22 @@ export class HeapSnapshotManager {
|
|
|
47
47
|
const snapshot = await this.getSnapshot(filePath);
|
|
48
48
|
return snapshot.staticData;
|
|
49
49
|
}
|
|
50
|
-
async
|
|
50
|
+
async getOrCreateIdForClassKey(filePath, classKey) {
|
|
51
51
|
const cached = this.#getCachedSnapshot(filePath);
|
|
52
|
-
let
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
cached.
|
|
56
|
-
cached.
|
|
52
|
+
let id = cached.classKeyToId.get(classKey);
|
|
53
|
+
if (!id) {
|
|
54
|
+
id = cached.idGenerator();
|
|
55
|
+
cached.classKeyToId.set(classKey, id);
|
|
56
|
+
cached.idToClassKey.set(id, classKey);
|
|
57
57
|
}
|
|
58
|
-
return
|
|
58
|
+
return id;
|
|
59
59
|
}
|
|
60
|
-
async
|
|
60
|
+
async getNodesById(filePath, id) {
|
|
61
61
|
const snapshot = await this.getSnapshot(filePath);
|
|
62
62
|
const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
|
|
63
|
-
const className = await this.
|
|
63
|
+
const className = await this.resolveClassKeyFromId(filePath, id);
|
|
64
64
|
if (!className) {
|
|
65
|
-
throw new Error(`Class with
|
|
65
|
+
throw new Error(`Class with ID ${id} not found in heap snapshot`);
|
|
66
66
|
}
|
|
67
67
|
const provider = snapshot.createNodesProviderForClass(className, filter);
|
|
68
68
|
return await provider.serializeItemsRange(0, Infinity);
|
|
@@ -99,9 +99,9 @@ export class HeapSnapshotManager {
|
|
|
99
99
|
}
|
|
100
100
|
return cached;
|
|
101
101
|
}
|
|
102
|
-
async
|
|
102
|
+
async resolveClassKeyFromId(filePath, id) {
|
|
103
103
|
const cached = this.#getCachedSnapshot(filePath);
|
|
104
|
-
return cached.
|
|
104
|
+
return cached.idToClassKey.get(id);
|
|
105
105
|
}
|
|
106
106
|
async #loadSnapshot(absolutePath) {
|
|
107
107
|
const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
|
package/build/src/McpContext.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
|
+
import fsPromises from 'node:fs/promises';
|
|
7
8
|
import os from 'node:os';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -14,7 +15,7 @@ import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
|
|
|
14
15
|
import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
|
|
15
16
|
import { listPages } from './tools/pages.js';
|
|
16
17
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
17
|
-
import { ensureExtension, getTempFilePath } from './utils/files.js';
|
|
18
|
+
import { ensureExtension, getTempFilePath, resolveCanonicalPath, } from './utils/files.js';
|
|
18
19
|
import { getNetworkMultiplierFromString } from './WaitForHelper.js';
|
|
19
20
|
const DEFAULT_TIMEOUT = 5_000;
|
|
20
21
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
@@ -106,7 +107,7 @@ export class McpContext {
|
|
|
106
107
|
setRoots(roots) {
|
|
107
108
|
this.#roots = roots;
|
|
108
109
|
}
|
|
109
|
-
validatePath(filePath) {
|
|
110
|
+
async validatePath(filePath) {
|
|
110
111
|
if (filePath === undefined) {
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
@@ -114,15 +115,36 @@ export class McpContext {
|
|
|
114
115
|
if (roots === undefined) {
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
+
let canonicalPath;
|
|
119
|
+
try {
|
|
120
|
+
canonicalPath = await resolveCanonicalPath(filePath);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
124
|
+
console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
|
|
125
|
+
throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
|
|
126
|
+
}
|
|
127
|
+
let allowed = false;
|
|
118
128
|
for (const root of roots) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
try {
|
|
130
|
+
const rootPathUri = root.uri;
|
|
131
|
+
const rootPath = path.resolve(fileURLToPath(rootPathUri));
|
|
132
|
+
const canonicalRoot = await fsPromises.realpath(rootPath);
|
|
133
|
+
if (canonicalPath === canonicalRoot ||
|
|
134
|
+
canonicalPath.startsWith(canonicalRoot + path.sep)) {
|
|
135
|
+
allowed = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (rootErr) {
|
|
140
|
+
const errMsg = rootErr instanceof Error ? rootErr.message : String(rootErr);
|
|
141
|
+
console.warn(`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`);
|
|
142
|
+
// Skip this root if it cannot be resolved.
|
|
123
143
|
}
|
|
124
144
|
}
|
|
125
|
-
|
|
145
|
+
if (!allowed) {
|
|
146
|
+
throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
|
|
147
|
+
}
|
|
126
148
|
}
|
|
127
149
|
resolveCdpRequestId(page, cdpRequestId) {
|
|
128
150
|
if (!cdpRequestId) {
|
|
@@ -213,12 +235,23 @@ export class McpContext {
|
|
|
213
235
|
await page.emulateNetworkConditions(networkCondition);
|
|
214
236
|
newSettings.networkConditions = options.networkConditions;
|
|
215
237
|
}
|
|
238
|
+
const secondarySession = this.getDevToolsUniverse(mcpPage)?.session;
|
|
216
239
|
if (!options.cpuThrottlingRate) {
|
|
217
240
|
await page.emulateCPUThrottling(1);
|
|
241
|
+
if (secondarySession) {
|
|
242
|
+
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
|
243
|
+
rate: 1,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
218
246
|
delete newSettings.cpuThrottlingRate;
|
|
219
247
|
}
|
|
220
248
|
else {
|
|
221
249
|
await page.emulateCPUThrottling(options.cpuThrottlingRate);
|
|
250
|
+
if (secondarySession) {
|
|
251
|
+
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
|
252
|
+
rate: options.cpuThrottlingRate,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
222
255
|
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
|
|
223
256
|
}
|
|
224
257
|
if (!options.geolocation) {
|
|
@@ -250,7 +283,6 @@ export class McpContext {
|
|
|
250
283
|
newSettings.colorScheme = options.colorScheme;
|
|
251
284
|
}
|
|
252
285
|
if (!options.viewport) {
|
|
253
|
-
await page.setViewport(null);
|
|
254
286
|
delete newSettings.viewport;
|
|
255
287
|
}
|
|
256
288
|
else {
|
|
@@ -260,14 +292,22 @@ export class McpContext {
|
|
|
260
292
|
hasTouch: false,
|
|
261
293
|
isLandscape: false,
|
|
262
294
|
};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
295
|
+
newSettings.viewport = { ...defaults, ...options.viewport };
|
|
296
|
+
}
|
|
297
|
+
if (options.extraHttpHeaders !== undefined) {
|
|
298
|
+
await page.setExtraHTTPHeaders(options.extraHttpHeaders);
|
|
299
|
+
newSettings.extraHttpHeaders = options.extraHttpHeaders;
|
|
300
|
+
if (Object.keys(options.extraHttpHeaders).length === 0) {
|
|
301
|
+
delete newSettings.extraHttpHeaders;
|
|
302
|
+
}
|
|
266
303
|
}
|
|
267
304
|
mcpPage.emulationSettings = Object.keys(newSettings).length
|
|
268
305
|
? newSettings
|
|
269
306
|
: {};
|
|
270
307
|
this.#updateSelectedPageTimeouts();
|
|
308
|
+
// This should happen after updating the page timeouts.
|
|
309
|
+
// Setting the viewport can trigger a reload which we don't want to timeout.
|
|
310
|
+
await page.setViewport(newSettings.viewport ?? null);
|
|
271
311
|
}
|
|
272
312
|
setIsRunningPerformanceTrace(x) {
|
|
273
313
|
this.#isRunningTrace = x;
|
|
@@ -333,9 +373,9 @@ export class McpContext {
|
|
|
333
373
|
page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
|
|
334
374
|
// 10sec should be enough for the load event to be emitted during
|
|
335
375
|
// navigations.
|
|
336
|
-
// Increased in case we throttle the network requests
|
|
376
|
+
// Increased in case we throttle the network requests or the CPU
|
|
337
377
|
const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
|
|
338
|
-
page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
|
|
378
|
+
page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier);
|
|
339
379
|
}
|
|
340
380
|
// Linear scan over per-page snapshots. The page count is small (typically
|
|
341
381
|
// 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
|
|
@@ -499,7 +539,7 @@ export class McpContext {
|
|
|
499
539
|
}
|
|
500
540
|
async saveTemporaryFile(data, filename) {
|
|
501
541
|
const filepath = await getTempFilePath(filename);
|
|
502
|
-
this.validatePath(filepath);
|
|
542
|
+
await this.validatePath(filepath);
|
|
503
543
|
try {
|
|
504
544
|
await fs.writeFile(filepath, data);
|
|
505
545
|
}
|
|
@@ -509,7 +549,7 @@ export class McpContext {
|
|
|
509
549
|
return { filepath };
|
|
510
550
|
}
|
|
511
551
|
async saveFile(data, clientProvidedFilePath, extension) {
|
|
512
|
-
this.validatePath(clientProvidedFilePath);
|
|
552
|
+
await this.validatePath(clientProvidedFilePath);
|
|
513
553
|
try {
|
|
514
554
|
const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
|
|
515
555
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -562,7 +602,6 @@ export class McpContext {
|
|
|
562
602
|
await this.#networkCollector.init(pages);
|
|
563
603
|
}
|
|
564
604
|
async installExtension(extensionPath) {
|
|
565
|
-
this.validatePath(extensionPath);
|
|
566
605
|
const id = await this.browser.installExtension(extensionPath);
|
|
567
606
|
return id;
|
|
568
607
|
}
|
|
@@ -586,20 +625,16 @@ export class McpContext {
|
|
|
586
625
|
return pptrExtensions.get(id);
|
|
587
626
|
}
|
|
588
627
|
async getHeapSnapshotAggregates(filePath) {
|
|
589
|
-
this.validatePath(filePath);
|
|
590
628
|
return await this.#heapSnapshotManager.getAggregates(filePath);
|
|
591
629
|
}
|
|
592
630
|
async getHeapSnapshotStats(filePath) {
|
|
593
|
-
this.validatePath(filePath);
|
|
594
631
|
return await this.#heapSnapshotManager.getStats(filePath);
|
|
595
632
|
}
|
|
596
633
|
async getHeapSnapshotStaticData(filePath) {
|
|
597
|
-
this.validatePath(filePath);
|
|
598
634
|
return await this.#heapSnapshotManager.getStaticData(filePath);
|
|
599
635
|
}
|
|
600
|
-
async
|
|
601
|
-
this.
|
|
602
|
-
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
|
|
636
|
+
async getHeapSnapshotNodesById(filePath, id) {
|
|
637
|
+
return await this.#heapSnapshotManager.getNodesById(filePath, id);
|
|
603
638
|
}
|
|
604
639
|
async getHeapSnapshotRetainers(filePath, nodeId) {
|
|
605
640
|
return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
|
package/build/src/McpResponse.js
CHANGED
|
@@ -505,7 +505,7 @@ export class McpResponse {
|
|
|
505
505
|
}
|
|
506
506
|
const geolocation = this.#page?.geolocation;
|
|
507
507
|
if (geolocation) {
|
|
508
|
-
response.push(`Emulating geolocation: latitude=${geolocation.latitude},
|
|
508
|
+
response.push(`Emulating geolocation: latitude=${geolocation.latitude}, longitude=${geolocation.longitude}`);
|
|
509
509
|
structuredContent.geolocation = geolocation;
|
|
510
510
|
}
|
|
511
511
|
const viewport = this.#page?.viewport;
|
|
@@ -136,6 +136,12 @@ export const commands = {
|
|
|
136
136
|
description: "Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.",
|
|
137
137
|
required: false,
|
|
138
138
|
},
|
|
139
|
+
extraHttpHeaders: {
|
|
140
|
+
name: 'extraHttpHeaders',
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: '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.',
|
|
143
|
+
required: false,
|
|
144
|
+
},
|
|
139
145
|
},
|
|
140
146
|
},
|
|
141
147
|
evaluate_script: {
|
|
@@ -240,8 +246,8 @@ export const commands = {
|
|
|
240
246
|
},
|
|
241
247
|
},
|
|
242
248
|
},
|
|
243
|
-
|
|
244
|
-
description: 'Loads a memory heapsnapshot and returns
|
|
249
|
+
get_heapsnapshot_class_nodes: {
|
|
250
|
+
description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs. (requires flag: --experimentalMemory=true)',
|
|
245
251
|
category: 'Memory',
|
|
246
252
|
args: {
|
|
247
253
|
filePath: {
|
|
@@ -250,45 +256,51 @@ export const commands = {
|
|
|
250
256
|
description: 'A path to a .heapsnapshot file to read.',
|
|
251
257
|
required: true,
|
|
252
258
|
},
|
|
259
|
+
id: {
|
|
260
|
+
name: 'id',
|
|
261
|
+
type: 'number',
|
|
262
|
+
description: 'The ID for the class, obtained from details.',
|
|
263
|
+
required: true,
|
|
264
|
+
},
|
|
253
265
|
pageIdx: {
|
|
254
266
|
name: 'pageIdx',
|
|
255
267
|
type: 'number',
|
|
256
|
-
description: 'The page index for pagination
|
|
268
|
+
description: 'The page index for pagination.',
|
|
257
269
|
required: false,
|
|
258
270
|
},
|
|
259
271
|
pageSize: {
|
|
260
272
|
name: 'pageSize',
|
|
261
273
|
type: 'number',
|
|
262
|
-
description: 'The page size for pagination
|
|
274
|
+
description: 'The page size for pagination.',
|
|
263
275
|
required: false,
|
|
264
276
|
},
|
|
265
277
|
},
|
|
266
278
|
},
|
|
267
|
-
|
|
268
|
-
description: '
|
|
269
|
-
category: '
|
|
279
|
+
get_heapsnapshot_details: {
|
|
280
|
+
description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates. (requires flag: --experimentalMemory=true)',
|
|
281
|
+
category: 'Memory',
|
|
270
282
|
args: {
|
|
271
|
-
|
|
272
|
-
name: '
|
|
273
|
-
type: 'number',
|
|
274
|
-
description: 'The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.',
|
|
275
|
-
required: false,
|
|
276
|
-
},
|
|
277
|
-
requestFilePath: {
|
|
278
|
-
name: 'requestFilePath',
|
|
283
|
+
filePath: {
|
|
284
|
+
name: 'filePath',
|
|
279
285
|
type: 'string',
|
|
280
|
-
description: '
|
|
286
|
+
description: 'A path to a .heapsnapshot file to read.',
|
|
287
|
+
required: true,
|
|
288
|
+
},
|
|
289
|
+
pageIdx: {
|
|
290
|
+
name: 'pageIdx',
|
|
291
|
+
type: 'number',
|
|
292
|
+
description: 'The page index for pagination of aggregates.',
|
|
281
293
|
required: false,
|
|
282
294
|
},
|
|
283
|
-
|
|
284
|
-
name: '
|
|
285
|
-
type: '
|
|
286
|
-
description: 'The
|
|
295
|
+
pageSize: {
|
|
296
|
+
name: 'pageSize',
|
|
297
|
+
type: 'number',
|
|
298
|
+
description: 'The page size for pagination of aggregates.',
|
|
287
299
|
required: false,
|
|
288
300
|
},
|
|
289
301
|
},
|
|
290
302
|
},
|
|
291
|
-
|
|
303
|
+
get_heapsnapshot_retainers: {
|
|
292
304
|
description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --experimentalMemory=true)',
|
|
293
305
|
category: 'Memory',
|
|
294
306
|
args: {
|
|
@@ -301,7 +313,7 @@ export const commands = {
|
|
|
301
313
|
nodeId: {
|
|
302
314
|
name: 'nodeId',
|
|
303
315
|
type: 'number',
|
|
304
|
-
description: 'The
|
|
316
|
+
description: 'The node ID to get retainers for.',
|
|
305
317
|
required: true,
|
|
306
318
|
},
|
|
307
319
|
pageIdx: {
|
|
@@ -318,8 +330,8 @@ export const commands = {
|
|
|
318
330
|
},
|
|
319
331
|
},
|
|
320
332
|
},
|
|
321
|
-
|
|
322
|
-
description: 'Loads a memory heapsnapshot and returns
|
|
333
|
+
get_heapsnapshot_summary: {
|
|
334
|
+
description: 'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --experimentalMemory=true)',
|
|
323
335
|
category: 'Memory',
|
|
324
336
|
args: {
|
|
325
337
|
filePath: {
|
|
@@ -328,22 +340,28 @@ export const commands = {
|
|
|
328
340
|
description: 'A path to a .heapsnapshot file to read.',
|
|
329
341
|
required: true,
|
|
330
342
|
},
|
|
331
|
-
|
|
332
|
-
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
get_network_request: {
|
|
346
|
+
description: 'Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.',
|
|
347
|
+
category: 'Network',
|
|
348
|
+
args: {
|
|
349
|
+
reqid: {
|
|
350
|
+
name: 'reqid',
|
|
333
351
|
type: 'number',
|
|
334
|
-
description: 'The
|
|
335
|
-
required:
|
|
352
|
+
description: 'The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.',
|
|
353
|
+
required: false,
|
|
336
354
|
},
|
|
337
|
-
|
|
338
|
-
name: '
|
|
339
|
-
type: '
|
|
340
|
-
description: 'The
|
|
355
|
+
requestFilePath: {
|
|
356
|
+
name: 'requestFilePath',
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.',
|
|
341
359
|
required: false,
|
|
342
360
|
},
|
|
343
|
-
|
|
344
|
-
name: '
|
|
345
|
-
type: '
|
|
346
|
-
description: 'The
|
|
361
|
+
responseFilePath: {
|
|
362
|
+
name: 'responseFilePath',
|
|
363
|
+
type: 'string',
|
|
364
|
+
description: 'The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.',
|
|
347
365
|
required: false,
|
|
348
366
|
},
|
|
349
367
|
},
|
|
@@ -507,18 +525,6 @@ export const commands = {
|
|
|
507
525
|
category: 'WebMCP',
|
|
508
526
|
args: {},
|
|
509
527
|
},
|
|
510
|
-
load_memory_snapshot: {
|
|
511
|
-
description: 'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --experimentalMemory=true)',
|
|
512
|
-
category: 'Memory',
|
|
513
|
-
args: {
|
|
514
|
-
filePath: {
|
|
515
|
-
name: 'filePath',
|
|
516
|
-
type: 'string',
|
|
517
|
-
description: 'A path to a .heapsnapshot file to read.',
|
|
518
|
-
required: true,
|
|
519
|
-
},
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
528
|
navigate_page: {
|
|
523
529
|
description: 'Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.',
|
|
524
530
|
category: 'Navigation automation',
|
|
@@ -732,7 +738,7 @@ export const commands = {
|
|
|
732
738
|
},
|
|
733
739
|
},
|
|
734
740
|
},
|
|
735
|
-
|
|
741
|
+
take_heapsnapshot: {
|
|
736
742
|
description: 'Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.',
|
|
737
743
|
category: 'Memory',
|
|
738
744
|
args: {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import '../polyfill.js';
|
|
7
7
|
import process from 'node:process';
|
|
8
|
+
import { closeBrowser } from '../browser.js';
|
|
8
9
|
import { createMcpServer, logDisclaimers } from '../index.js';
|
|
9
10
|
import { logger, saveLogsToFile } from '../logger.js';
|
|
10
11
|
import { ClearcutLogger } from '../telemetry/ClearcutLogger.js';
|
|
@@ -21,6 +22,43 @@ if (process.env['CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT'] !== 'true') {
|
|
|
21
22
|
logger('Unhandled promise rejection', promise, reason);
|
|
22
23
|
});
|
|
23
24
|
}
|
|
25
|
+
// Shutdown on stdin EOF (stdio MCP convention — the client closes the
|
|
26
|
+
// transport to signal exit) and on standard termination signals. Without
|
|
27
|
+
// this, an active Chrome subprocess keeps the Node event loop ref'd after
|
|
28
|
+
// stdin closes and the server hangs until something else kills it.
|
|
29
|
+
let shuttingDown = false;
|
|
30
|
+
async function shutdown(reason) {
|
|
31
|
+
if (shuttingDown) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
shuttingDown = true;
|
|
35
|
+
logger(`Shutting down (${reason})`);
|
|
36
|
+
// Backstop in case browser teardown hangs (e.g. unresponsive Chrome,
|
|
37
|
+
// slow beforeunload handlers, many tabs). Exits 0 because we still
|
|
38
|
+
// honored the shutdown request; the log line preserves observability.
|
|
39
|
+
// Unref'd so it doesn't keep the loop alive on the clean path.
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
logger('Shutdown timeout exceeded, forcing exit');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}, 10000).unref();
|
|
44
|
+
await closeBrowser();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
process.stdin.on('end', () => {
|
|
48
|
+
void shutdown('stdin end');
|
|
49
|
+
});
|
|
50
|
+
process.stdin.on('close', () => {
|
|
51
|
+
void shutdown('stdin close');
|
|
52
|
+
});
|
|
53
|
+
process.on('SIGTERM', () => {
|
|
54
|
+
void shutdown('SIGTERM');
|
|
55
|
+
});
|
|
56
|
+
process.on('SIGINT', () => {
|
|
57
|
+
void shutdown('SIGINT');
|
|
58
|
+
});
|
|
59
|
+
process.on('SIGHUP', () => {
|
|
60
|
+
void shutdown('SIGHUP');
|
|
61
|
+
});
|
|
24
62
|
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
|
25
63
|
const { server } = await createMcpServer(args, {
|
|
26
64
|
logFile,
|
package/build/src/browser.js
CHANGED
|
@@ -10,6 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { logger } from './logger.js';
|
|
11
11
|
import { puppeteer } from './third_party/index.js';
|
|
12
12
|
let browser;
|
|
13
|
+
let browserMode;
|
|
13
14
|
function makeTargetFilter(enableExtensions = false) {
|
|
14
15
|
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
|
|
15
16
|
if (!enableExtensions) {
|
|
@@ -95,7 +96,12 @@ export async function ensureBrowserConnected(options) {
|
|
|
95
96
|
}
|
|
96
97
|
logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
|
|
97
98
|
try {
|
|
98
|
-
browser
|
|
99
|
+
// Assign mode before browser so a concurrent closeBrowser() never sees
|
|
100
|
+
// `browser` set with `browserMode` still undefined (would fall through
|
|
101
|
+
// to the disconnect() path and orphan a launched Chrome).
|
|
102
|
+
const connected = await puppeteer.connect(connectOptions);
|
|
103
|
+
browserMode = 'connected';
|
|
104
|
+
browser = connected;
|
|
99
105
|
}
|
|
100
106
|
catch (err) {
|
|
101
107
|
throw new Error(`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`, {
|
|
@@ -198,7 +204,35 @@ export async function ensureBrowserLaunched(options) {
|
|
|
198
204
|
if (browser?.connected) {
|
|
199
205
|
return browser;
|
|
200
206
|
}
|
|
201
|
-
browser
|
|
207
|
+
// Assign mode before browser; see the connect path above for rationale.
|
|
208
|
+
const launched = await launch(options);
|
|
209
|
+
browserMode = 'launched';
|
|
210
|
+
browser = launched;
|
|
202
211
|
return browser;
|
|
203
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Shutdown hook for the active browser. Closes a launched browser (so the
|
|
215
|
+
* Chrome subprocess is reaped) or disconnects from an attached browser (so
|
|
216
|
+
* the user's Chrome instance stays alive). No-op if no browser is active or
|
|
217
|
+
* the connection has already been dropped. Called from the server entrypoint
|
|
218
|
+
* on stdin EOF / SIGTERM / SIGINT.
|
|
219
|
+
*/
|
|
220
|
+
export async function closeBrowser() {
|
|
221
|
+
const b = browser;
|
|
222
|
+
const mode = browserMode;
|
|
223
|
+
browser = undefined;
|
|
224
|
+
browserMode = undefined;
|
|
225
|
+
if (!b || !b.connected) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (mode === 'launched') {
|
|
229
|
+
await b.close().catch(err => {
|
|
230
|
+
logger('Failed to close browser', err);
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await b.disconnect().catch(err => {
|
|
235
|
+
logger('Failed to disconnect from browser', err);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
204
238
|
//# sourceMappingURL=browser.js.map
|