chrome-devtools-mcp 0.17.3 → 0.18.1
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 +27 -2
- package/build/src/McpContext.js +206 -73
- package/build/src/McpResponse.js +38 -2
- package/build/src/SlimMcpResponse.js +18 -0
- package/build/src/browser.js +20 -0
- package/build/src/cli.js +12 -0
- package/build/src/daemon/daemon.js +167 -0
- package/build/src/daemon/utils.js +67 -0
- package/build/src/formatters/ConsoleFormatter.js +106 -105
- package/build/src/formatters/IssueFormatter.js +50 -48
- package/build/src/main.js +18 -11
- package/build/src/third_party/THIRD_PARTY_NOTICES +1 -1
- package/build/src/third_party/bundled-packages.json +2 -2
- package/build/src/third_party/index.js +107 -28
- package/build/src/third_party/issue-descriptions/corsLocalNetworkAccessPermissionDenied.md +2 -2
- package/build/src/tools/emulation.js +1 -83
- package/build/src/tools/input.js +26 -0
- package/build/src/tools/memory.js +28 -0
- package/build/src/tools/pages.js +22 -14
- package/build/src/tools/screencast.js +79 -0
- package/build/src/tools/slim/tools.js +81 -0
- package/build/src/tools/snapshot.js +5 -2
- package/build/src/tools/tools.js +34 -16
- package/build/src/version.js +9 -0
- package/package.json +6 -6
- package/build/src/third_party/issue-descriptions/corsInsecurePrivateNetwork.md +0 -10
- package/build/src/third_party/issue-descriptions/corsPreflightAllowPrivateNetworkError.md +0 -10
- package/build/src/third_party/issue-descriptions/corsPrivateNetworkPermissionDenied.md +0 -10
package/README.md
CHANGED
|
@@ -73,6 +73,21 @@ Add the following config to your MCP client:
|
|
|
73
73
|
> [!NOTE]
|
|
74
74
|
> Using `chrome-devtools-mcp@latest` ensures that your MCP client will always use the latest version of the Chrome DevTools MCP server.
|
|
75
75
|
|
|
76
|
+
If you are intersted in doing only basic browser tasks, use the `--slim` mode:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"chrome-devtools": {
|
|
82
|
+
"command": "npx",
|
|
83
|
+
"args": ["-y", "chrome-devtools-mcp@latest", "--slim", "--headless"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [Slim tool reference](./docs/slim-tool-reference.md).
|
|
90
|
+
|
|
76
91
|
### MCP Client configuration
|
|
77
92
|
|
|
78
93
|
<details>
|
|
@@ -399,7 +414,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
399
414
|
|
|
400
415
|
<!-- BEGIN AUTO GENERATED TOOLS -->
|
|
401
416
|
|
|
402
|
-
- **Input automation** (
|
|
417
|
+
- **Input automation** (9 tools)
|
|
403
418
|
- [`click`](docs/tool-reference.md#click)
|
|
404
419
|
- [`drag`](docs/tool-reference.md#drag)
|
|
405
420
|
- [`fill`](docs/tool-reference.md#fill)
|
|
@@ -407,6 +422,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
407
422
|
- [`handle_dialog`](docs/tool-reference.md#handle_dialog)
|
|
408
423
|
- [`hover`](docs/tool-reference.md#hover)
|
|
409
424
|
- [`press_key`](docs/tool-reference.md#press_key)
|
|
425
|
+
- [`type_text`](docs/tool-reference.md#type_text)
|
|
410
426
|
- [`upload_file`](docs/tool-reference.md#upload_file)
|
|
411
427
|
- **Navigation automation** (6 tools)
|
|
412
428
|
- [`close_page`](docs/tool-reference.md#close_page)
|
|
@@ -418,10 +434,11 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
418
434
|
- **Emulation** (2 tools)
|
|
419
435
|
- [`emulate`](docs/tool-reference.md#emulate)
|
|
420
436
|
- [`resize_page`](docs/tool-reference.md#resize_page)
|
|
421
|
-
- **Performance** (
|
|
437
|
+
- **Performance** (4 tools)
|
|
422
438
|
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
|
|
423
439
|
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
|
|
424
440
|
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)
|
|
441
|
+
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
|
|
425
442
|
- **Network** (2 tools)
|
|
426
443
|
- [`get_network_request`](docs/tool-reference.md#get_network_request)
|
|
427
444
|
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)
|
|
@@ -495,6 +512,10 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
495
512
|
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
|
|
496
513
|
- **Type:** boolean
|
|
497
514
|
|
|
515
|
+
- **`--experimentalScreencast`/ `--experimental-screencast`**
|
|
516
|
+
Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
|
|
517
|
+
- **Type:** boolean
|
|
518
|
+
|
|
498
519
|
- **`--chromeArg`/ `--chrome-arg`**
|
|
499
520
|
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
|
|
500
521
|
- **Type:** array
|
|
@@ -528,6 +549,10 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
528
549
|
- **Type:** boolean
|
|
529
550
|
- **Default:** `true`
|
|
530
551
|
|
|
552
|
+
- **`--slim`**
|
|
553
|
+
Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
|
|
554
|
+
- **Type:** boolean
|
|
555
|
+
|
|
531
556
|
<!-- END AUTO GENERATED OPTIONS -->
|
|
532
557
|
|
|
533
558
|
Pass them via the `args` property in the JSON configuration. For example:
|
package/build/src/McpContext.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
import { extractUrlLikeFromDevToolsTitle, UniverseManager, urlsEqual, } from './DevtoolsUtils.js';
|
|
10
10
|
import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
|
|
11
11
|
import { Locator } from './third_party/index.js';
|
|
12
|
+
import { PredefinedNetworkConditions } from './third_party/index.js';
|
|
12
13
|
import { listPages } from './tools/pages.js';
|
|
13
14
|
import { takeSnapshot } from './tools/snapshot.js';
|
|
14
15
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
@@ -44,26 +45,30 @@ function getExtensionFromMimeType(mimeType) {
|
|
|
44
45
|
export class McpContext {
|
|
45
46
|
browser;
|
|
46
47
|
logger;
|
|
47
|
-
//
|
|
48
|
+
// Maps LLM-provided isolatedContext name → Puppeteer BrowserContext.
|
|
49
|
+
#isolatedContexts = new Map();
|
|
50
|
+
// Reverse lookup: Page → isolatedContext name (for snapshot labeling).
|
|
51
|
+
// WeakMap so closed pages are garbage-collected automatically.
|
|
52
|
+
#pageToIsolatedContextName = new WeakMap();
|
|
53
|
+
// Auto-generated name counter for when no name is provided.
|
|
54
|
+
#nextIsolatedContextId = 1;
|
|
48
55
|
#pages = [];
|
|
56
|
+
#extensionServiceWorkers = [];
|
|
49
57
|
#pageToDevToolsPage = new Map();
|
|
50
58
|
#selectedPage;
|
|
51
|
-
// The most recent snapshot.
|
|
52
59
|
#textSnapshot = null;
|
|
53
60
|
#networkCollector;
|
|
54
61
|
#consoleCollector;
|
|
55
62
|
#devtoolsUniverseManager;
|
|
56
63
|
#extensionRegistry = new ExtensionRegistry();
|
|
57
64
|
#isRunningTrace = false;
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#geolocationMap = new WeakMap();
|
|
61
|
-
#viewportMap = new WeakMap();
|
|
62
|
-
#userAgentMap = new WeakMap();
|
|
63
|
-
#colorSchemeMap = new WeakMap();
|
|
65
|
+
#screenRecorderData = null;
|
|
66
|
+
#emulationSettingsMap = new WeakMap();
|
|
64
67
|
#dialog;
|
|
65
68
|
#pageIdMap = new WeakMap();
|
|
66
69
|
#nextPageId = 1;
|
|
70
|
+
#extensionServiceWorkerMap = new WeakMap();
|
|
71
|
+
#nextExtensionServiceWorkerId = 1;
|
|
67
72
|
#nextSnapshotId = 1;
|
|
68
73
|
#traceResults = [];
|
|
69
74
|
#locatorClass;
|
|
@@ -92,6 +97,7 @@ export class McpContext {
|
|
|
92
97
|
}
|
|
93
98
|
async #init() {
|
|
94
99
|
const pages = await this.createPagesSnapshot();
|
|
100
|
+
await this.createExtensionServiceWorkersSnapshot();
|
|
95
101
|
await this.#networkCollector.init(pages);
|
|
96
102
|
await this.#consoleCollector.init(pages);
|
|
97
103
|
await this.#devtoolsUniverseManager.init(pages);
|
|
@@ -100,6 +106,10 @@ export class McpContext {
|
|
|
100
106
|
this.#networkCollector.dispose();
|
|
101
107
|
this.#consoleCollector.dispose();
|
|
102
108
|
this.#devtoolsUniverseManager.dispose();
|
|
109
|
+
// Isolated contexts are intentionally not closed here.
|
|
110
|
+
// Either the entire browser will be closed or we disconnect
|
|
111
|
+
// without destroying browser state.
|
|
112
|
+
this.#isolatedContexts.clear();
|
|
103
113
|
}
|
|
104
114
|
static async from(browser, logger, opts,
|
|
105
115
|
/* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
|
|
@@ -163,8 +173,20 @@ export class McpContext {
|
|
|
163
173
|
getConsoleMessageById(id) {
|
|
164
174
|
return this.#consoleCollector.getById(this.getSelectedPage(), id);
|
|
165
175
|
}
|
|
166
|
-
async newPage(background) {
|
|
167
|
-
|
|
176
|
+
async newPage(background, isolatedContextName) {
|
|
177
|
+
let page;
|
|
178
|
+
if (isolatedContextName !== undefined) {
|
|
179
|
+
let ctx = this.#isolatedContexts.get(isolatedContextName);
|
|
180
|
+
if (!ctx) {
|
|
181
|
+
ctx = await this.browser.createBrowserContext();
|
|
182
|
+
this.#isolatedContexts.set(isolatedContextName, ctx);
|
|
183
|
+
}
|
|
184
|
+
page = await ctx.newPage();
|
|
185
|
+
this.#pageToIsolatedContextName.set(page, isolatedContextName);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
page = await this.browser.newPage({ background });
|
|
189
|
+
}
|
|
168
190
|
await this.createPagesSnapshot();
|
|
169
191
|
this.selectPage(page);
|
|
170
192
|
this.#networkCollector.addPage(page);
|
|
@@ -177,84 +199,133 @@ export class McpContext {
|
|
|
177
199
|
}
|
|
178
200
|
const page = this.getPageById(pageId);
|
|
179
201
|
await page.close({ runBeforeUnload: false });
|
|
202
|
+
this.#pageToIsolatedContextName.delete(page);
|
|
180
203
|
}
|
|
181
204
|
getNetworkRequestById(reqid) {
|
|
182
205
|
return this.#networkCollector.getById(this.getSelectedPage(), reqid);
|
|
183
206
|
}
|
|
184
|
-
|
|
207
|
+
async emulate(options) {
|
|
185
208
|
const page = this.getSelectedPage();
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
const currentSettings = this.#emulationSettingsMap.get(page) ?? {};
|
|
210
|
+
const newSettings = { ...currentSettings };
|
|
211
|
+
let timeoutsNeedUpdate = false;
|
|
212
|
+
if (options.networkConditions !== undefined) {
|
|
213
|
+
timeoutsNeedUpdate = true;
|
|
214
|
+
if (options.networkConditions === null ||
|
|
215
|
+
options.networkConditions === 'No emulation') {
|
|
216
|
+
await page.emulateNetworkConditions(null);
|
|
217
|
+
delete newSettings.networkConditions;
|
|
218
|
+
}
|
|
219
|
+
else if (options.networkConditions === 'Offline') {
|
|
220
|
+
await page.emulateNetworkConditions({
|
|
221
|
+
offline: true,
|
|
222
|
+
download: 0,
|
|
223
|
+
upload: 0,
|
|
224
|
+
latency: 0,
|
|
225
|
+
});
|
|
226
|
+
newSettings.networkConditions = 'Offline';
|
|
227
|
+
}
|
|
228
|
+
else if (options.networkConditions in PredefinedNetworkConditions) {
|
|
229
|
+
const networkCondition = PredefinedNetworkConditions[options.networkConditions];
|
|
230
|
+
await page.emulateNetworkConditions(networkCondition);
|
|
231
|
+
newSettings.networkConditions = options.networkConditions;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (options.cpuThrottlingRate !== undefined) {
|
|
235
|
+
timeoutsNeedUpdate = true;
|
|
236
|
+
if (options.cpuThrottlingRate === null) {
|
|
237
|
+
await page.emulateCPUThrottling(1);
|
|
238
|
+
delete newSettings.cpuThrottlingRate;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
await page.emulateCPUThrottling(options.cpuThrottlingRate);
|
|
242
|
+
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (options.geolocation !== undefined) {
|
|
246
|
+
if (options.geolocation === null) {
|
|
247
|
+
await page.setGeolocation({ latitude: 0, longitude: 0 });
|
|
248
|
+
delete newSettings.geolocation;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
await page.setGeolocation(options.geolocation);
|
|
252
|
+
newSettings.geolocation = options.geolocation;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (options.userAgent !== undefined) {
|
|
256
|
+
if (options.userAgent === null) {
|
|
257
|
+
await page.setUserAgent({ userAgent: undefined });
|
|
258
|
+
delete newSettings.userAgent;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
await page.setUserAgent({ userAgent: options.userAgent });
|
|
262
|
+
newSettings.userAgent = options.userAgent;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (options.colorScheme !== undefined) {
|
|
266
|
+
if (options.colorScheme === null || options.colorScheme === 'auto') {
|
|
267
|
+
await page.emulateMediaFeatures([
|
|
268
|
+
{ name: 'prefers-color-scheme', value: '' },
|
|
269
|
+
]);
|
|
270
|
+
delete newSettings.colorScheme;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
await page.emulateMediaFeatures([
|
|
274
|
+
{ name: 'prefers-color-scheme', value: options.colorScheme },
|
|
275
|
+
]);
|
|
276
|
+
newSettings.colorScheme = options.colorScheme;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (options.viewport !== undefined) {
|
|
280
|
+
if (options.viewport === null) {
|
|
281
|
+
await page.setViewport(null);
|
|
282
|
+
delete newSettings.viewport;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const defaults = {
|
|
286
|
+
deviceScaleFactor: 1,
|
|
287
|
+
isMobile: false,
|
|
288
|
+
hasTouch: false,
|
|
289
|
+
isLandscape: false,
|
|
290
|
+
};
|
|
291
|
+
const viewport = { ...defaults, ...options.viewport };
|
|
292
|
+
await page.setViewport(viewport);
|
|
293
|
+
newSettings.viewport = viewport;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (Object.keys(newSettings).length) {
|
|
297
|
+
this.#emulationSettingsMap.set(page, newSettings);
|
|
188
298
|
}
|
|
189
299
|
else {
|
|
190
|
-
this.#
|
|
300
|
+
this.#emulationSettingsMap.delete(page);
|
|
301
|
+
}
|
|
302
|
+
if (timeoutsNeedUpdate) {
|
|
303
|
+
this.#updateSelectedPageTimeouts();
|
|
191
304
|
}
|
|
192
|
-
this.#updateSelectedPageTimeouts();
|
|
193
305
|
}
|
|
194
306
|
getNetworkConditions() {
|
|
195
307
|
const page = this.getSelectedPage();
|
|
196
|
-
return this.#
|
|
197
|
-
}
|
|
198
|
-
setCpuThrottlingRate(rate) {
|
|
199
|
-
const page = this.getSelectedPage();
|
|
200
|
-
this.#cpuThrottlingRateMap.set(page, rate);
|
|
201
|
-
this.#updateSelectedPageTimeouts();
|
|
308
|
+
return this.#emulationSettingsMap.get(page)?.networkConditions ?? null;
|
|
202
309
|
}
|
|
203
310
|
getCpuThrottlingRate() {
|
|
204
311
|
const page = this.getSelectedPage();
|
|
205
|
-
return this.#
|
|
206
|
-
}
|
|
207
|
-
setGeolocation(geolocation) {
|
|
208
|
-
const page = this.getSelectedPage();
|
|
209
|
-
if (geolocation === null) {
|
|
210
|
-
this.#geolocationMap.delete(page);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
this.#geolocationMap.set(page, geolocation);
|
|
214
|
-
}
|
|
312
|
+
return this.#emulationSettingsMap.get(page)?.cpuThrottlingRate ?? 1;
|
|
215
313
|
}
|
|
216
314
|
getGeolocation() {
|
|
217
315
|
const page = this.getSelectedPage();
|
|
218
|
-
return this.#
|
|
219
|
-
}
|
|
220
|
-
setViewport(viewport) {
|
|
221
|
-
const page = this.getSelectedPage();
|
|
222
|
-
if (viewport === null) {
|
|
223
|
-
this.#viewportMap.delete(page);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
this.#viewportMap.set(page, viewport);
|
|
227
|
-
}
|
|
316
|
+
return this.#emulationSettingsMap.get(page)?.geolocation ?? null;
|
|
228
317
|
}
|
|
229
318
|
getViewport() {
|
|
230
319
|
const page = this.getSelectedPage();
|
|
231
|
-
return this.#
|
|
232
|
-
}
|
|
233
|
-
setUserAgent(userAgent) {
|
|
234
|
-
const page = this.getSelectedPage();
|
|
235
|
-
if (userAgent === null) {
|
|
236
|
-
this.#userAgentMap.delete(page);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
this.#userAgentMap.set(page, userAgent);
|
|
240
|
-
}
|
|
320
|
+
return this.#emulationSettingsMap.get(page)?.viewport ?? null;
|
|
241
321
|
}
|
|
242
322
|
getUserAgent() {
|
|
243
323
|
const page = this.getSelectedPage();
|
|
244
|
-
return this.#
|
|
245
|
-
}
|
|
246
|
-
setColorScheme(scheme) {
|
|
247
|
-
const page = this.getSelectedPage();
|
|
248
|
-
if (scheme === null) {
|
|
249
|
-
this.#colorSchemeMap.delete(page);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
this.#colorSchemeMap.set(page, scheme);
|
|
253
|
-
}
|
|
324
|
+
return this.#emulationSettingsMap.get(page)?.userAgent ?? null;
|
|
254
325
|
}
|
|
255
326
|
getColorScheme() {
|
|
256
327
|
const page = this.getSelectedPage();
|
|
257
|
-
return this.#
|
|
328
|
+
return this.#emulationSettingsMap.get(page)?.colorScheme ?? null;
|
|
258
329
|
}
|
|
259
330
|
setIsRunningPerformanceTrace(x) {
|
|
260
331
|
this.#isRunningTrace = x;
|
|
@@ -262,6 +333,12 @@ export class McpContext {
|
|
|
262
333
|
isRunningPerformanceTrace() {
|
|
263
334
|
return this.#isRunningTrace;
|
|
264
335
|
}
|
|
336
|
+
getScreenRecorder() {
|
|
337
|
+
return this.#screenRecorderData;
|
|
338
|
+
}
|
|
339
|
+
setScreenRecorder(data) {
|
|
340
|
+
this.#screenRecorderData = data;
|
|
341
|
+
}
|
|
265
342
|
isCruxEnabled() {
|
|
266
343
|
return this.#options.performanceCrux;
|
|
267
344
|
}
|
|
@@ -277,7 +354,7 @@ export class McpContext {
|
|
|
277
354
|
throw new Error('No page selected');
|
|
278
355
|
}
|
|
279
356
|
if (page.isClosed()) {
|
|
280
|
-
throw new Error(`The selected page has been closed. Call ${listPages.name} to see open pages.`);
|
|
357
|
+
throw new Error(`The selected page has been closed. Call ${listPages().name} to see open pages.`);
|
|
281
358
|
}
|
|
282
359
|
return page;
|
|
283
360
|
}
|
|
@@ -354,18 +431,36 @@ export class McpContext {
|
|
|
354
431
|
}
|
|
355
432
|
}
|
|
356
433
|
/**
|
|
357
|
-
* Creates a snapshot of the
|
|
434
|
+
* Creates a snapshot of the extension service workers.
|
|
358
435
|
*/
|
|
436
|
+
async createExtensionServiceWorkersSnapshot() {
|
|
437
|
+
const allTargets = await this.browser.targets();
|
|
438
|
+
const serviceWorkers = allTargets.filter(target => {
|
|
439
|
+
return (target.type() === 'service_worker' &&
|
|
440
|
+
target.url().includes('chrome-extension://'));
|
|
441
|
+
});
|
|
442
|
+
for (const serviceWorker of serviceWorkers) {
|
|
443
|
+
if (!this.#extensionServiceWorkerMap.has(serviceWorker)) {
|
|
444
|
+
this.#extensionServiceWorkerMap.set(serviceWorker, 'sw-' + this.#nextExtensionServiceWorkerId++);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => {
|
|
448
|
+
return {
|
|
449
|
+
target: serviceWorker,
|
|
450
|
+
id: this.#extensionServiceWorkerMap.get(serviceWorker),
|
|
451
|
+
url: serviceWorker.url(),
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
return this.#extensionServiceWorkers;
|
|
455
|
+
}
|
|
359
456
|
async createPagesSnapshot() {
|
|
360
|
-
const allPages = await this
|
|
457
|
+
const allPages = await this.#getAllPages();
|
|
361
458
|
for (const page of allPages) {
|
|
362
459
|
if (!this.#pageIdMap.has(page)) {
|
|
363
460
|
this.#pageIdMap.set(page, this.#nextPageId++);
|
|
364
461
|
}
|
|
365
462
|
}
|
|
366
463
|
this.#pages = allPages.filter(page => {
|
|
367
|
-
// If we allow debugging DevTools windows, return all pages.
|
|
368
|
-
// If we are in regular mode, the user should only see non-DevTools page.
|
|
369
464
|
return (this.#options.experimentalDevToolsDebugging ||
|
|
370
465
|
!page.url().startsWith('devtools://'));
|
|
371
466
|
});
|
|
@@ -376,9 +471,37 @@ export class McpContext {
|
|
|
376
471
|
await this.detectOpenDevToolsWindows();
|
|
377
472
|
return this.#pages;
|
|
378
473
|
}
|
|
474
|
+
async #getAllPages() {
|
|
475
|
+
const defaultCtx = this.browser.defaultBrowserContext();
|
|
476
|
+
const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
477
|
+
// Build a reverse lookup from BrowserContext instance → name.
|
|
478
|
+
const contextToName = new Map();
|
|
479
|
+
for (const [name, ctx] of this.#isolatedContexts) {
|
|
480
|
+
contextToName.set(ctx, name);
|
|
481
|
+
}
|
|
482
|
+
// Auto-discover BrowserContexts not in our mapping (e.g., externally
|
|
483
|
+
// created incognito contexts) and assign generated names.
|
|
484
|
+
const knownContexts = new Set(this.#isolatedContexts.values());
|
|
485
|
+
for (const ctx of this.browser.browserContexts()) {
|
|
486
|
+
if (ctx !== defaultCtx && !ctx.closed && !knownContexts.has(ctx)) {
|
|
487
|
+
const name = `isolated-context-${this.#nextIsolatedContextId++}`;
|
|
488
|
+
this.#isolatedContexts.set(name, ctx);
|
|
489
|
+
contextToName.set(ctx, name);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Use page.browserContext() to determine each page's context membership.
|
|
493
|
+
for (const page of allPages) {
|
|
494
|
+
const ctx = page.browserContext();
|
|
495
|
+
const name = contextToName.get(ctx);
|
|
496
|
+
if (name) {
|
|
497
|
+
this.#pageToIsolatedContextName.set(page, name);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return allPages;
|
|
501
|
+
}
|
|
379
502
|
async detectOpenDevToolsWindows() {
|
|
380
503
|
this.logger('Detecting open DevTools windows');
|
|
381
|
-
const pages = await this
|
|
504
|
+
const pages = await this.#getAllPages();
|
|
382
505
|
this.#pageToDevToolsPage = new Map();
|
|
383
506
|
for (const devToolsPage of pages) {
|
|
384
507
|
if (devToolsPage.url().startsWith('devtools://')) {
|
|
@@ -406,9 +529,18 @@ export class McpContext {
|
|
|
406
529
|
}
|
|
407
530
|
}
|
|
408
531
|
}
|
|
532
|
+
getExtensionServiceWorkers() {
|
|
533
|
+
return this.#extensionServiceWorkers;
|
|
534
|
+
}
|
|
535
|
+
getExtensionServiceWorkerId(extensionServiceWorker) {
|
|
536
|
+
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
|
|
537
|
+
}
|
|
409
538
|
getPages() {
|
|
410
539
|
return this.#pages;
|
|
411
540
|
}
|
|
541
|
+
getIsolatedContextName(page) {
|
|
542
|
+
return this.#pageToIsolatedContextName.get(page);
|
|
543
|
+
}
|
|
412
544
|
getDevToolsPage(page) {
|
|
413
545
|
return this.#pageToDevToolsPage.get(page);
|
|
414
546
|
}
|
|
@@ -560,10 +692,10 @@ export class McpContext {
|
|
|
560
692
|
waitForTextOnPage(text, timeout) {
|
|
561
693
|
const page = this.getSelectedPage();
|
|
562
694
|
const frames = page.frames();
|
|
563
|
-
let locator = this.#locatorClass.race(frames.flatMap(frame => [
|
|
564
|
-
frame.locator(`aria/${
|
|
565
|
-
frame.locator(`text/${
|
|
566
|
-
]));
|
|
695
|
+
let locator = this.#locatorClass.race(frames.flatMap(frame => text.flatMap(value => [
|
|
696
|
+
frame.locator(`aria/${value}`),
|
|
697
|
+
frame.locator(`text/${value}`),
|
|
698
|
+
])));
|
|
567
699
|
if (timeout) {
|
|
568
700
|
locator = locator.setTimeout(timeout);
|
|
569
701
|
}
|
|
@@ -583,7 +715,8 @@ export class McpContext {
|
|
|
583
715
|
},
|
|
584
716
|
};
|
|
585
717
|
});
|
|
586
|
-
await this.#
|
|
718
|
+
const pages = await this.#getAllPages();
|
|
719
|
+
await this.#networkCollector.init(pages);
|
|
587
720
|
}
|
|
588
721
|
async installExtension(extensionPath) {
|
|
589
722
|
const id = await this.browser.installExtension(extensionPath);
|
package/build/src/McpResponse.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
|
|
|
14
14
|
import { paginate } from './utils/pagination.js';
|
|
15
15
|
export class McpResponse {
|
|
16
16
|
#includePages = false;
|
|
17
|
+
#includeExtensionServiceWorkers = false;
|
|
17
18
|
#snapshotParams;
|
|
18
19
|
#attachedNetworkRequestId;
|
|
19
20
|
#attachedNetworkRequestOptions;
|
|
@@ -27,6 +28,10 @@ export class McpResponse {
|
|
|
27
28
|
#listExtensions;
|
|
28
29
|
#devToolsData;
|
|
29
30
|
#tabId;
|
|
31
|
+
#args;
|
|
32
|
+
constructor(args) {
|
|
33
|
+
this.#args = args;
|
|
34
|
+
}
|
|
30
35
|
attachDevToolsData(data) {
|
|
31
36
|
this.#devToolsData = data;
|
|
32
37
|
}
|
|
@@ -35,6 +40,9 @@ export class McpResponse {
|
|
|
35
40
|
}
|
|
36
41
|
setIncludePages(value) {
|
|
37
42
|
this.#includePages = value;
|
|
43
|
+
if (this.#args.categoryExtensions) {
|
|
44
|
+
this.#includeExtensionServiceWorkers = value;
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
includeSnapshot(params) {
|
|
40
48
|
this.#snapshotParams = params ?? {
|
|
@@ -142,6 +150,9 @@ export class McpResponse {
|
|
|
142
150
|
if (this.#includePages) {
|
|
143
151
|
await context.createPagesSnapshot();
|
|
144
152
|
}
|
|
153
|
+
if (this.#includeExtensionServiceWorkers) {
|
|
154
|
+
await context.createExtensionServiceWorkersSnapshot();
|
|
155
|
+
}
|
|
145
156
|
let snapshot;
|
|
146
157
|
if (this.#snapshotParams) {
|
|
147
158
|
await context.createTextSnapshot(this.#snapshotParams.verbose, this.#devToolsData);
|
|
@@ -325,15 +336,40 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
325
336
|
if (this.#includePages) {
|
|
326
337
|
const parts = [`## Pages`];
|
|
327
338
|
for (const page of context.getPages()) {
|
|
328
|
-
|
|
339
|
+
const isolatedContextName = context.getIsolatedContextName(page);
|
|
340
|
+
const contextLabel = isolatedContextName
|
|
341
|
+
? ` isolatedContext=${isolatedContextName}`
|
|
342
|
+
: '';
|
|
343
|
+
parts.push(`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`);
|
|
329
344
|
}
|
|
330
345
|
response.push(...parts);
|
|
331
346
|
structuredContent.pages = context.getPages().map(page => {
|
|
332
|
-
|
|
347
|
+
const isolatedContextName = context.getIsolatedContextName(page);
|
|
348
|
+
const entry = {
|
|
333
349
|
id: context.getPageId(page),
|
|
334
350
|
url: page.url(),
|
|
335
351
|
selected: context.isPageSelected(page),
|
|
336
352
|
};
|
|
353
|
+
if (isolatedContextName) {
|
|
354
|
+
entry.isolatedContext = isolatedContextName;
|
|
355
|
+
}
|
|
356
|
+
return entry;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (this.#includeExtensionServiceWorkers) {
|
|
360
|
+
if (!context.getExtensionServiceWorkers().length) {
|
|
361
|
+
response.push(`## Extension Service Workers`);
|
|
362
|
+
}
|
|
363
|
+
for (const extensionServiceWorker of context.getExtensionServiceWorkers()) {
|
|
364
|
+
response.push(`${extensionServiceWorker.id}: ${extensionServiceWorker.url}`);
|
|
365
|
+
}
|
|
366
|
+
structuredContent.extensionServiceWorkers = context
|
|
367
|
+
.getExtensionServiceWorkers()
|
|
368
|
+
.map(extensionServiceWorker => {
|
|
369
|
+
return {
|
|
370
|
+
id: extensionServiceWorker.id,
|
|
371
|
+
url: extensionServiceWorker.url,
|
|
372
|
+
};
|
|
337
373
|
});
|
|
338
374
|
}
|
|
339
375
|
if (this.#tabId) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { McpResponse } from './McpResponse.js';
|
|
7
|
+
export class SlimMcpResponse extends McpResponse {
|
|
8
|
+
async handle(_toolName, _context) {
|
|
9
|
+
const text = {
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: this.responseLines.join('\n'),
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
content: [text],
|
|
15
|
+
structuredContent: text,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
package/build/src/browser.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
6
7
|
import fs from 'node:fs';
|
|
7
8
|
import os from 'node:os';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -103,6 +104,22 @@ export async function ensureBrowserConnected(options) {
|
|
|
103
104
|
logger('Connected Puppeteer');
|
|
104
105
|
return browser;
|
|
105
106
|
}
|
|
107
|
+
export function detectDisplay() {
|
|
108
|
+
// Only detect display on Linux/UNIX.
|
|
109
|
+
if (os.platform() === 'win32' || os.platform() === 'darwin') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!process.env['DISPLAY']) {
|
|
113
|
+
try {
|
|
114
|
+
const result = execSync(`ps -u $(id -u) -o pid= | xargs -I{} cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^DISPLAY=' | cut -d= -f2`);
|
|
115
|
+
const display = result.toString('utf8').trim();
|
|
116
|
+
process.env['DISPLAY'] = display;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// no-op
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
106
123
|
export async function launch(options) {
|
|
107
124
|
const { channel, executablePath, headless, isolated } = options;
|
|
108
125
|
const profileDirName = channel && channel !== 'stable'
|
|
@@ -133,6 +150,9 @@ export async function launch(options) {
|
|
|
133
150
|
? `chrome-${channel}`
|
|
134
151
|
: 'chrome';
|
|
135
152
|
}
|
|
153
|
+
if (!headless) {
|
|
154
|
+
detectDisplay();
|
|
155
|
+
}
|
|
136
156
|
try {
|
|
137
157
|
const browser = await puppeteer.launch({
|
|
138
158
|
channel: puppeteerChannel,
|
package/build/src/cli.js
CHANGED
|
@@ -159,6 +159,10 @@ export const cliOptions = {
|
|
|
159
159
|
describe: 'Whether to enable interoperability tools',
|
|
160
160
|
hidden: true,
|
|
161
161
|
},
|
|
162
|
+
experimentalScreencast: {
|
|
163
|
+
type: 'boolean',
|
|
164
|
+
describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
|
|
165
|
+
},
|
|
162
166
|
chromeArg: {
|
|
163
167
|
type: 'array',
|
|
164
168
|
describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
|
|
@@ -213,6 +217,10 @@ export const cliOptions = {
|
|
|
213
217
|
hidden: true,
|
|
214
218
|
describe: 'Include watchdog PID in Clearcut request headers (for testing).',
|
|
215
219
|
},
|
|
220
|
+
slim: {
|
|
221
|
+
type: 'boolean',
|
|
222
|
+
describe: 'Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.',
|
|
223
|
+
},
|
|
216
224
|
};
|
|
217
225
|
export function parseArguments(version, argv = process.argv) {
|
|
218
226
|
const yargsInstance = yargs(hideBin(argv))
|
|
@@ -286,6 +294,10 @@ export function parseArguments(version, argv = process.argv) {
|
|
|
286
294
|
'$0 --no-performance-crux',
|
|
287
295
|
'Disable CrUX (field data) integration in performance tools.',
|
|
288
296
|
],
|
|
297
|
+
[
|
|
298
|
+
'$0 --slim',
|
|
299
|
+
'Only 3 tools: navigation, JavaScript execution and screenshot',
|
|
300
|
+
],
|
|
289
301
|
]);
|
|
290
302
|
return yargsInstance
|
|
291
303
|
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|