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 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** (8 tools)
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** (3 tools)
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:
@@ -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
- // The most recent page state.
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
- #networkConditionsMap = new WeakMap();
59
- #cpuThrottlingRateMap = new WeakMap();
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
- const page = await this.browser.newPage({ background });
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
- setNetworkConditions(conditions) {
207
+ async emulate(options) {
185
208
  const page = this.getSelectedPage();
186
- if (conditions === null) {
187
- this.#networkConditionsMap.delete(page);
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.#networkConditionsMap.set(page, conditions);
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.#networkConditionsMap.get(page) ?? null;
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.#cpuThrottlingRateMap.get(page) ?? 1;
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.#geolocationMap.get(page) ?? null;
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.#viewportMap.get(page) ?? null;
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.#userAgentMap.get(page) ?? null;
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.#colorSchemeMap.get(page) ?? null;
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 pages.
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.browser.pages(this.#options.experimentalIncludeAllPages);
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.browser.pages(this.#options.experimentalIncludeAllPages);
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/${text}`),
565
- frame.locator(`text/${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.#networkCollector.init(await this.browser.pages());
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);
@@ -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
- parts.push(`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}`);
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
- return {
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
+ }
@@ -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()))