chrome-devtools-mcp 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -5
  2. package/build/src/McpContext.js +242 -266
  3. package/build/src/McpPage.js +95 -0
  4. package/build/src/McpResponse.js +124 -48
  5. package/build/src/bin/chrome-devtools-cli-options.js +651 -0
  6. package/build/src/{cli.js → bin/chrome-devtools-mcp-cli-options.js} +12 -2
  7. package/build/src/bin/chrome-devtools-mcp-main.js +35 -0
  8. package/build/src/bin/chrome-devtools-mcp.js +21 -0
  9. package/build/src/bin/chrome-devtools.js +185 -0
  10. package/build/src/bin/cliDefinitions.js +615 -0
  11. package/build/src/browser.js +13 -12
  12. package/build/src/daemon/client.js +152 -0
  13. package/build/src/daemon/daemon.js +56 -17
  14. package/build/src/daemon/types.js +6 -0
  15. package/build/src/daemon/utils.js +57 -16
  16. package/build/src/index.js +204 -16
  17. package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
  18. package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
  19. package/build/src/third_party/bundled-packages.json +4 -3
  20. package/build/src/third_party/devtools-formatter-worker.js +5 -14
  21. package/build/src/third_party/index.js +2128 -472
  22. package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +7 -0
  23. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
  24. package/build/src/tools/ToolDefinition.js +52 -0
  25. package/build/src/tools/console.js +3 -3
  26. package/build/src/tools/emulation.js +13 -45
  27. package/build/src/tools/extensions.js +17 -0
  28. package/build/src/tools/input.js +33 -33
  29. package/build/src/tools/lighthouse.js +123 -0
  30. package/build/src/tools/memory.js +5 -5
  31. package/build/src/tools/network.js +7 -7
  32. package/build/src/tools/pages.js +32 -32
  33. package/build/src/tools/performance.js +16 -14
  34. package/build/src/tools/screencast.js +5 -5
  35. package/build/src/tools/screenshot.js +6 -6
  36. package/build/src/tools/script.js +99 -49
  37. package/build/src/tools/slim/tools.js +18 -18
  38. package/build/src/tools/snapshot.js +5 -4
  39. package/build/src/tools/tools.js +2 -0
  40. package/build/src/types.js +6 -0
  41. package/build/src/utils/files.js +19 -0
  42. package/build/src/version.js +1 -1
  43. package/package.json +15 -9
  44. package/build/src/main.js +0 -203
@@ -4,16 +4,16 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import fs from 'node:fs/promises';
7
- import os from 'node:os';
8
7
  import path from 'node:path';
9
8
  import { extractUrlLikeFromDevToolsTitle, UniverseManager, urlsEqual, } from './DevtoolsUtils.js';
9
+ import { McpPage } from './McpPage.js';
10
10
  import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
11
11
  import { Locator } from './third_party/index.js';
12
12
  import { PredefinedNetworkConditions } from './third_party/index.js';
13
13
  import { listPages } from './tools/pages.js';
14
- import { takeSnapshot } from './tools/snapshot.js';
15
14
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
16
15
  import { ExtensionRegistry, } from './utils/ExtensionRegistry.js';
16
+ import { saveTemporaryFile } from './utils/files.js';
17
17
  import { WaitForHelper } from './WaitForHelper.js';
18
18
  const DEFAULT_TIMEOUT = 5_000;
19
19
  const NAVIGATION_TIMEOUT = 10_000;
@@ -31,49 +31,31 @@ function getNetworkMultiplierFromString(condition) {
31
31
  }
32
32
  return 1;
33
33
  }
34
- function getExtensionFromMimeType(mimeType) {
35
- switch (mimeType) {
36
- case 'image/png':
37
- return 'png';
38
- case 'image/jpeg':
39
- return 'jpeg';
40
- case 'image/webp':
41
- return 'webp';
42
- }
43
- throw new Error(`No mapping for Mime type ${mimeType}.`);
44
- }
45
34
  export class McpContext {
46
35
  browser;
47
36
  logger;
48
37
  // Maps LLM-provided isolatedContext name → Puppeteer BrowserContext.
49
38
  #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
39
  // Auto-generated name counter for when no name is provided.
54
40
  #nextIsolatedContextId = 1;
55
41
  #pages = [];
56
42
  #extensionServiceWorkers = [];
57
- #pageToDevToolsPage = new Map();
43
+ #mcpPages = new Map();
58
44
  #selectedPage;
59
- #textSnapshot = null;
60
45
  #networkCollector;
61
46
  #consoleCollector;
62
47
  #devtoolsUniverseManager;
63
48
  #extensionRegistry = new ExtensionRegistry();
64
49
  #isRunningTrace = false;
65
50
  #screenRecorderData = null;
66
- #emulationSettingsMap = new WeakMap();
67
- #dialog;
68
- #pageIdMap = new WeakMap();
69
51
  #nextPageId = 1;
52
+ #extensionPages = new WeakMap();
70
53
  #extensionServiceWorkerMap = new WeakMap();
71
54
  #nextExtensionServiceWorkerId = 1;
72
55
  #nextSnapshotId = 1;
73
56
  #traceResults = [];
74
57
  #locatorClass;
75
58
  #options;
76
- #uniqueBackendNodeIdToMcpId = new Map();
77
59
  constructor(browser, logger, options, locatorClass) {
78
60
  this.browser = browser;
79
61
  this.logger = logger;
@@ -106,6 +88,10 @@ export class McpContext {
106
88
  this.#networkCollector.dispose();
107
89
  this.#consoleCollector.dispose();
108
90
  this.#devtoolsUniverseManager.dispose();
91
+ for (const mcpPage of this.#mcpPages.values()) {
92
+ mcpPage.dispose();
93
+ }
94
+ this.#mcpPages.clear();
109
95
  // Isolated contexts are intentionally not closed here.
110
96
  // Either the entire browser will be closed or we disconnect
111
97
  // without destroying browser state.
@@ -118,13 +104,12 @@ export class McpContext {
118
104
  await context.#init();
119
105
  return context;
120
106
  }
121
- resolveCdpRequestId(cdpRequestId) {
122
- const selectedPage = this.getSelectedPage();
107
+ resolveCdpRequestId(page, cdpRequestId) {
123
108
  if (!cdpRequestId) {
124
109
  this.logger('no network request');
125
110
  return;
126
111
  }
127
- const request = this.#networkCollector.find(selectedPage, request => {
112
+ const request = this.#networkCollector.find(page.pptrPage, request => {
128
113
  // @ts-expect-error id is internal.
129
114
  return request.id === cdpRequestId;
130
115
  });
@@ -134,17 +119,18 @@ export class McpContext {
134
119
  }
135
120
  return this.#networkCollector.getIdForResource(request);
136
121
  }
137
- resolveCdpElementId(cdpBackendNodeId) {
122
+ resolveCdpElementId(page, cdpBackendNodeId) {
138
123
  if (!cdpBackendNodeId) {
139
124
  this.logger('no cdpBackendNodeId');
140
125
  return;
141
126
  }
142
- if (this.#textSnapshot === null) {
127
+ const snapshot = page.textSnapshot;
128
+ if (!snapshot) {
143
129
  this.logger('no text snapshot');
144
130
  return;
145
131
  }
146
132
  // TODO: index by backendNodeId instead.
147
- const queue = [this.#textSnapshot.root];
133
+ const queue = [snapshot.root];
148
134
  while (queue.length) {
149
135
  const current = queue.pop();
150
136
  if (current.backendNodeId === cdpBackendNodeId) {
@@ -156,22 +142,20 @@ export class McpContext {
156
142
  }
157
143
  return;
158
144
  }
159
- getNetworkRequests(includePreservedRequests) {
160
- const page = this.getSelectedPage();
161
- return this.#networkCollector.getData(page, includePreservedRequests);
145
+ getNetworkRequests(page, includePreservedRequests) {
146
+ return this.#networkCollector.getData(page.pptrPage, includePreservedRequests);
162
147
  }
163
- getConsoleData(includePreservedMessages) {
164
- const page = this.getSelectedPage();
165
- return this.#consoleCollector.getData(page, includePreservedMessages);
148
+ getConsoleData(page, includePreservedMessages) {
149
+ return this.#consoleCollector.getData(page.pptrPage, includePreservedMessages);
166
150
  }
167
- getDevToolsUniverse() {
168
- return this.#devtoolsUniverseManager.get(this.getSelectedPage());
151
+ getDevToolsUniverse(page) {
152
+ return this.#devtoolsUniverseManager.get(page.pptrPage);
169
153
  }
170
154
  getConsoleMessageStableId(message) {
171
155
  return this.#consoleCollector.getIdForResource(message);
172
156
  }
173
- getConsoleMessageById(id) {
174
- return this.#consoleCollector.getById(this.getSelectedPage(), id);
157
+ getConsoleMessageById(page, id) {
158
+ return this.#consoleCollector.getById(page.pptrPage, id);
175
159
  }
176
160
  async newPage(background, isolatedContextName) {
177
161
  let page;
@@ -182,150 +166,111 @@ export class McpContext {
182
166
  this.#isolatedContexts.set(isolatedContextName, ctx);
183
167
  }
184
168
  page = await ctx.newPage();
185
- this.#pageToIsolatedContextName.set(page, isolatedContextName);
186
169
  }
187
170
  else {
188
171
  page = await this.browser.newPage({ background });
189
172
  }
190
173
  await this.createPagesSnapshot();
191
- this.selectPage(page);
174
+ this.selectPage(this.#getMcpPage(page));
192
175
  this.#networkCollector.addPage(page);
193
176
  this.#consoleCollector.addPage(page);
194
- return page;
177
+ return this.#getMcpPage(page);
195
178
  }
196
179
  async closePage(pageId) {
197
180
  if (this.#pages.length === 1) {
198
181
  throw new Error(CLOSE_PAGE_ERROR);
199
182
  }
200
183
  const page = this.getPageById(pageId);
201
- await page.close({ runBeforeUnload: false });
202
- this.#pageToIsolatedContextName.delete(page);
203
- }
204
- getNetworkRequestById(reqid) {
205
- return this.#networkCollector.getById(this.getSelectedPage(), reqid);
206
- }
207
- async emulate(options) {
208
- const page = this.getSelectedPage();
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
- }
184
+ if (page) {
185
+ page.dispose();
186
+ this.#mcpPages.delete(page.pptrPage);
187
+ }
188
+ await page.pptrPage.close({ runBeforeUnload: false });
189
+ }
190
+ getNetworkRequestById(page, reqid) {
191
+ return this.#networkCollector.getById(page.pptrPage, reqid);
192
+ }
193
+ async restoreEmulation(page) {
194
+ const currentSetting = page.emulationSettings;
195
+ await this.emulate(currentSetting, page.pptrPage);
196
+ }
197
+ async emulate(options, targetPage) {
198
+ const page = targetPage ?? this.getSelectedPptrPage();
199
+ const mcpPage = this.#getMcpPage(page);
200
+ const newSettings = { ...mcpPage.emulationSettings };
201
+ if (!options.networkConditions) {
202
+ await page.emulateNetworkConditions(null);
203
+ delete newSettings.networkConditions;
204
+ }
205
+ else if (options.networkConditions === 'Offline') {
206
+ await page.emulateNetworkConditions({
207
+ offline: true,
208
+ download: 0,
209
+ upload: 0,
210
+ latency: 0,
211
+ });
212
+ newSettings.networkConditions = 'Offline';
233
213
  }
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
- }
214
+ else if (options.networkConditions in PredefinedNetworkConditions) {
215
+ const networkCondition = PredefinedNetworkConditions[options.networkConditions];
216
+ await page.emulateNetworkConditions(networkCondition);
217
+ newSettings.networkConditions = options.networkConditions;
244
218
  }
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
- }
219
+ if (!options.cpuThrottlingRate) {
220
+ await page.emulateCPUThrottling(1);
221
+ delete newSettings.cpuThrottlingRate;
254
222
  }
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
- }
223
+ else {
224
+ await page.emulateCPUThrottling(options.cpuThrottlingRate);
225
+ newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
264
226
  }
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
- }
227
+ if (!options.geolocation) {
228
+ await page.setGeolocation({ latitude: 0, longitude: 0 });
229
+ delete newSettings.geolocation;
278
230
  }
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
- }
231
+ else {
232
+ await page.setGeolocation(options.geolocation);
233
+ newSettings.geolocation = options.geolocation;
295
234
  }
296
- if (Object.keys(newSettings).length) {
297
- this.#emulationSettingsMap.set(page, newSettings);
235
+ if (!options.userAgent) {
236
+ await page.setUserAgent({ userAgent: undefined });
237
+ delete newSettings.userAgent;
298
238
  }
299
239
  else {
300
- this.#emulationSettingsMap.delete(page);
240
+ await page.setUserAgent({ userAgent: options.userAgent });
241
+ newSettings.userAgent = options.userAgent;
301
242
  }
302
- if (timeoutsNeedUpdate) {
303
- this.#updateSelectedPageTimeouts();
243
+ if (!options.colorScheme || options.colorScheme === 'auto') {
244
+ await page.emulateMediaFeatures([
245
+ { name: 'prefers-color-scheme', value: '' },
246
+ ]);
247
+ delete newSettings.colorScheme;
304
248
  }
305
- }
306
- getNetworkConditions() {
307
- const page = this.getSelectedPage();
308
- return this.#emulationSettingsMap.get(page)?.networkConditions ?? null;
309
- }
310
- getCpuThrottlingRate() {
311
- const page = this.getSelectedPage();
312
- return this.#emulationSettingsMap.get(page)?.cpuThrottlingRate ?? 1;
313
- }
314
- getGeolocation() {
315
- const page = this.getSelectedPage();
316
- return this.#emulationSettingsMap.get(page)?.geolocation ?? null;
317
- }
318
- getViewport() {
319
- const page = this.getSelectedPage();
320
- return this.#emulationSettingsMap.get(page)?.viewport ?? null;
321
- }
322
- getUserAgent() {
323
- const page = this.getSelectedPage();
324
- return this.#emulationSettingsMap.get(page)?.userAgent ?? null;
325
- }
326
- getColorScheme() {
327
- const page = this.getSelectedPage();
328
- return this.#emulationSettingsMap.get(page)?.colorScheme ?? null;
249
+ else {
250
+ await page.emulateMediaFeatures([
251
+ { name: 'prefers-color-scheme', value: options.colorScheme },
252
+ ]);
253
+ newSettings.colorScheme = options.colorScheme;
254
+ }
255
+ if (!options.viewport) {
256
+ await page.setViewport(null);
257
+ delete newSettings.viewport;
258
+ }
259
+ else {
260
+ const defaults = {
261
+ deviceScaleFactor: 1,
262
+ isMobile: false,
263
+ hasTouch: false,
264
+ isLandscape: false,
265
+ };
266
+ const viewport = { ...defaults, ...options.viewport };
267
+ await page.setViewport(viewport);
268
+ newSettings.viewport = viewport;
269
+ }
270
+ mcpPage.emulationSettings = Object.keys(newSettings).length
271
+ ? newSettings
272
+ : {};
273
+ this.#updateSelectedPageTimeouts();
329
274
  }
330
275
  setIsRunningPerformanceTrace(x) {
331
276
  this.#isRunningTrace = x;
@@ -342,93 +287,70 @@ export class McpContext {
342
287
  isCruxEnabled() {
343
288
  return this.#options.performanceCrux;
344
289
  }
345
- getDialog() {
346
- return this.#dialog;
347
- }
348
- clearDialog() {
349
- this.#dialog = undefined;
350
- }
351
- getSelectedPage() {
290
+ getSelectedPptrPage() {
352
291
  const page = this.#selectedPage;
353
292
  if (!page) {
354
293
  throw new Error('No page selected');
355
294
  }
356
- if (page.isClosed()) {
295
+ if (page.pptrPage.isClosed()) {
357
296
  throw new Error(`The selected page has been closed. Call ${listPages().name} to see open pages.`);
358
297
  }
359
- return page;
298
+ return page.pptrPage;
299
+ }
300
+ getSelectedMcpPage() {
301
+ const page = this.getSelectedPptrPage();
302
+ return this.#getMcpPage(page);
360
303
  }
361
304
  getPageById(pageId) {
362
- const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
305
+ const page = this.#mcpPages.values().find(mcpPage => mcpPage.id === pageId);
363
306
  if (!page) {
364
307
  throw new Error('No page found');
365
308
  }
366
309
  return page;
367
310
  }
368
311
  getPageId(page) {
369
- return this.#pageIdMap.get(page);
312
+ return this.#mcpPages.get(page)?.id;
313
+ }
314
+ #getMcpPage(page) {
315
+ const mcpPage = this.#mcpPages.get(page);
316
+ if (!mcpPage) {
317
+ throw new Error('No McpPage found for the given page.');
318
+ }
319
+ return mcpPage;
320
+ }
321
+ #getSelectedMcpPage() {
322
+ return this.#getMcpPage(this.getSelectedPptrPage());
370
323
  }
371
- #dialogHandler = (dialog) => {
372
- this.#dialog = dialog;
373
- };
374
324
  isPageSelected(page) {
375
- return this.#selectedPage === page;
325
+ return this.#selectedPage?.pptrPage === page;
376
326
  }
377
327
  selectPage(newPage) {
378
- const oldPage = this.#selectedPage;
379
- if (oldPage) {
380
- oldPage.off('dialog', this.#dialogHandler);
381
- void oldPage.emulateFocusedPage(false).catch(error => {
382
- this.logger('Error turning off focused page emulation', error);
383
- });
384
- }
385
328
  this.#selectedPage = newPage;
386
- newPage.on('dialog', this.#dialogHandler);
387
329
  this.#updateSelectedPageTimeouts();
388
- void newPage.emulateFocusedPage(true).catch(error => {
389
- this.logger('Error turning on focused page emulation', error);
390
- });
391
330
  }
392
331
  #updateSelectedPageTimeouts() {
393
- const page = this.getSelectedPage();
332
+ const page = this.#getSelectedMcpPage();
394
333
  // For waiters 5sec timeout should be sufficient.
395
334
  // Increased in case we throttle the CPU
396
- const cpuMultiplier = this.getCpuThrottlingRate();
397
- page.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
335
+ const cpuMultiplier = page.cpuThrottlingRate;
336
+ page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
398
337
  // 10sec should be enough for the load event to be emitted during
399
338
  // navigations.
400
339
  // Increased in case we throttle the network requests
401
- const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
402
- page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
403
- }
404
- getNavigationTimeout() {
405
- const page = this.getSelectedPage();
406
- return page.getDefaultNavigationTimeout();
340
+ const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
341
+ page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
407
342
  }
343
+ // Linear scan over per-page snapshots. The page count is small (typically
344
+ // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
345
+ // complexity it would introduce.
408
346
  getAXNodeByUid(uid) {
409
- return this.#textSnapshot?.idToNode.get(uid);
410
- }
411
- async getElementByUid(uid) {
412
- if (!this.#textSnapshot?.idToNode.size) {
413
- throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
414
- }
415
- const node = this.#textSnapshot?.idToNode.get(uid);
416
- if (!node) {
417
- throw new Error('No such element found in the snapshot.');
418
- }
419
- const message = `Element with uid ${uid} no longer exists on the page.`;
420
- try {
421
- const handle = await node.elementHandle();
422
- if (!handle) {
423
- throw new Error(message);
347
+ for (const mcpPage of this.#mcpPages.values()) {
348
+ const node = mcpPage.textSnapshot?.idToNode.get(uid);
349
+ if (node) {
350
+ return node;
424
351
  }
425
- return handle;
426
- }
427
- catch (error) {
428
- throw new Error(message, {
429
- cause: error,
430
- });
431
352
  }
353
+ return undefined;
432
354
  }
433
355
  /**
434
356
  * Creates a snapshot of the extension service workers.
@@ -454,19 +376,35 @@ export class McpContext {
454
376
  return this.#extensionServiceWorkers;
455
377
  }
456
378
  async createPagesSnapshot() {
457
- const allPages = await this.#getAllPages();
379
+ const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
458
380
  for (const page of allPages) {
459
- if (!this.#pageIdMap.has(page)) {
460
- this.#pageIdMap.set(page, this.#nextPageId++);
381
+ let mcpPage = this.#mcpPages.get(page);
382
+ if (!mcpPage) {
383
+ mcpPage = new McpPage(page, this.#nextPageId++);
384
+ this.#mcpPages.set(page, mcpPage);
385
+ // We emulate a focused page for all pages to support multi-agent workflows.
386
+ void page.emulateFocusedPage(true).catch(error => {
387
+ this.logger('Error turning on focused page emulation', error);
388
+ });
389
+ }
390
+ mcpPage.isolatedContextName = isolatedContextNames.get(page);
391
+ }
392
+ // Prune orphaned #mcpPages entries (pages that no longer exist).
393
+ const currentPages = new Set(allPages);
394
+ for (const [page, mcpPage] of this.#mcpPages) {
395
+ if (!currentPages.has(page)) {
396
+ mcpPage.dispose();
397
+ this.#mcpPages.delete(page);
461
398
  }
462
399
  }
463
400
  this.#pages = allPages.filter(page => {
464
401
  return (this.#options.experimentalDevToolsDebugging ||
465
402
  !page.url().startsWith('devtools://'));
466
403
  });
467
- if ((!this.#selectedPage || this.#pages.indexOf(this.#selectedPage) === -1) &&
404
+ if ((!this.#selectedPage ||
405
+ this.#pages.indexOf(this.#selectedPage.pptrPage) === -1) &&
468
406
  this.#pages[0]) {
469
- this.selectPage(this.#pages[0]);
407
+ this.selectPage(this.#getMcpPage(this.#pages[0]));
470
408
  }
471
409
  await this.detectOpenDevToolsWindows();
472
410
  return this.#pages;
@@ -474,6 +412,32 @@ export class McpContext {
474
412
  async #getAllPages() {
475
413
  const defaultCtx = this.browser.defaultBrowserContext();
476
414
  const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
415
+ const allTargets = this.browser.targets();
416
+ const extensionTargets = allTargets.filter(target => {
417
+ return (target.url().startsWith('chrome-extension://') &&
418
+ target.type() === 'page');
419
+ });
420
+ for (const target of extensionTargets) {
421
+ // Right now target.page() returns null for popup and side panel pages.
422
+ let page = await target.page();
423
+ if (!page) {
424
+ // We need to cache pages instances for targets because target.asPage()
425
+ // returns a new page instance every time.
426
+ page = this.#extensionPages.get(target) ?? null;
427
+ if (!page) {
428
+ try {
429
+ page = await target.asPage();
430
+ this.#extensionPages.set(target, page);
431
+ }
432
+ catch (e) {
433
+ this.logger('Failed to get page for extension target', e);
434
+ }
435
+ }
436
+ }
437
+ if (page && !allPages.includes(page)) {
438
+ allPages.push(page);
439
+ }
440
+ }
477
441
  // Build a reverse lookup from BrowserContext instance → name.
478
442
  const contextToName = new Map();
479
443
  for (const [name, ctx] of this.#isolatedContexts) {
@@ -489,20 +453,24 @@ export class McpContext {
489
453
  contextToName.set(ctx, name);
490
454
  }
491
455
  }
492
- // Use page.browserContext() to determine each page's context membership.
456
+ // Map each page to its isolated context name (if any).
457
+ const isolatedContextNames = new Map();
493
458
  for (const page of allPages) {
494
459
  const ctx = page.browserContext();
495
460
  const name = contextToName.get(ctx);
496
461
  if (name) {
497
- this.#pageToIsolatedContextName.set(page, name);
462
+ isolatedContextNames.set(page, name);
498
463
  }
499
464
  }
500
- return allPages;
465
+ return { pages: allPages, isolatedContextNames };
501
466
  }
502
467
  async detectOpenDevToolsWindows() {
503
468
  this.logger('Detecting open DevTools windows');
504
- const pages = await this.#getAllPages();
505
- this.#pageToDevToolsPage = new Map();
469
+ const { pages } = await this.#getAllPages();
470
+ // Clear all devToolsPage references before re-detecting.
471
+ for (const mcpPage of this.#mcpPages.values()) {
472
+ mcpPage.devToolsPage = undefined;
473
+ }
506
474
  for (const devToolsPage of pages) {
507
475
  if (devToolsPage.url().startsWith('devtools://')) {
508
476
  try {
@@ -519,7 +487,10 @@ export class McpContext {
519
487
  // TODO: lookup without a loop.
520
488
  for (const page of this.#pages) {
521
489
  if (urlsEqual(page.url(), urlLike)) {
522
- this.#pageToDevToolsPage.set(page, devToolsPage);
490
+ const mcpPage = this.#mcpPages.get(page);
491
+ if (mcpPage) {
492
+ mcpPage.devToolsPage = devToolsPage;
493
+ }
523
494
  }
524
495
  }
525
496
  }
@@ -539,16 +510,15 @@ export class McpContext {
539
510
  return this.#pages;
540
511
  }
541
512
  getIsolatedContextName(page) {
542
- return this.#pageToIsolatedContextName.get(page);
513
+ return this.#mcpPages.get(page)?.isolatedContextName;
543
514
  }
544
515
  getDevToolsPage(page) {
545
- return this.#pageToDevToolsPage.get(page);
516
+ return this.#mcpPages.get(page)?.devToolsPage;
546
517
  }
547
- async getDevToolsData() {
518
+ async getDevToolsData(page) {
548
519
  try {
549
520
  this.logger('Getting DevTools UI data');
550
- const selectedPage = this.getSelectedPage();
551
- const devtoolsPage = this.getDevToolsPage(selectedPage);
521
+ const devtoolsPage = this.getDevToolsPage(page.pptrPage);
552
522
  if (!devtoolsPage) {
553
523
  this.logger('No DevTools page detected');
554
524
  return {};
@@ -575,15 +545,15 @@ export class McpContext {
575
545
  /**
576
546
  * Creates a text snapshot of a page.
577
547
  */
578
- async createTextSnapshot(verbose = false, devtoolsData = undefined) {
579
- const page = this.getSelectedPage();
580
- const rootNode = await page.accessibility.snapshot({
548
+ async createTextSnapshot(page, verbose = false, devtoolsData = undefined) {
549
+ const rootNode = await page.pptrPage.accessibility.snapshot({
581
550
  includeIframes: true,
582
551
  interestingOnly: !verbose,
583
552
  });
584
553
  if (!rootNode) {
585
554
  return;
586
555
  }
556
+ const { uniqueBackendNodeIdToMcpId } = page;
587
557
  const snapshotId = this.#nextSnapshotId++;
588
558
  // Iterate through the whole accessibility node tree and assign node ids that
589
559
  // will be used for the tree serialization and mapping ids back to nodes.
@@ -594,14 +564,14 @@ export class McpContext {
594
564
  let id = '';
595
565
  // @ts-expect-error untyped loaderId & backendNodeId.
596
566
  const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
597
- if (this.#uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
567
+ if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
598
568
  // Re-use MCP exposed ID if the uniqueId is the same.
599
- id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
569
+ id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
600
570
  }
601
571
  else {
602
572
  // Only generate a new ID if we have not seen the node before.
603
573
  id = `${snapshotId}_${idCounter++}`;
604
- this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
574
+ uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
605
575
  }
606
576
  seenUniqueIds.add(uniqueBackendId);
607
577
  const nodeWithId = {
@@ -623,49 +593,39 @@ export class McpContext {
623
593
  return nodeWithId;
624
594
  };
625
595
  const rootNodeWithId = assignIds(rootNode);
626
- this.#textSnapshot = {
596
+ const snapshot = {
627
597
  root: rootNodeWithId,
628
598
  snapshotId: String(snapshotId),
629
599
  idToNode,
630
600
  hasSelectedElement: false,
631
601
  verbose,
632
602
  };
633
- const data = devtoolsData ?? (await this.getDevToolsData());
603
+ page.textSnapshot = snapshot;
604
+ const data = devtoolsData ?? (await this.getDevToolsData(page));
634
605
  if (data?.cdpBackendNodeId) {
635
- this.#textSnapshot.hasSelectedElement = true;
636
- this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
606
+ snapshot.hasSelectedElement = true;
607
+ snapshot.selectedElementUid = this.resolveCdpElementId(page, data?.cdpBackendNodeId);
637
608
  }
638
609
  // Clean up unique IDs that we did not see anymore.
639
- for (const key of this.#uniqueBackendNodeIdToMcpId.keys()) {
610
+ for (const key of uniqueBackendNodeIdToMcpId.keys()) {
640
611
  if (!seenUniqueIds.has(key)) {
641
- this.#uniqueBackendNodeIdToMcpId.delete(key);
612
+ uniqueBackendNodeIdToMcpId.delete(key);
642
613
  }
643
614
  }
644
615
  }
645
- getTextSnapshot() {
646
- return this.#textSnapshot;
647
- }
648
- async saveTemporaryFile(data, mimeType) {
649
- try {
650
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
651
- const filename = path.join(dir, `screenshot.${getExtensionFromMimeType(mimeType)}`);
652
- await fs.writeFile(filename, data);
653
- return { filename };
654
- }
655
- catch (err) {
656
- this.logger(err);
657
- throw new Error('Could not save a screenshot to a file', { cause: err });
658
- }
616
+ async saveTemporaryFile(data, filename) {
617
+ return await saveTemporaryFile(data, filename);
659
618
  }
660
619
  async saveFile(data, filename) {
661
620
  try {
662
621
  const filePath = path.resolve(filename);
622
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
663
623
  await fs.writeFile(filePath, data);
664
- return { filename };
624
+ return { filename: filePath };
665
625
  }
666
626
  catch (err) {
667
627
  this.logger(err);
668
- throw new Error('Could not save a screenshot to a file', { cause: err });
628
+ throw new Error('Could not save a file', { cause: err });
669
629
  }
670
630
  }
671
631
  storeTraceRecording(result) {
@@ -680,17 +640,17 @@ export class McpContext {
680
640
  return new WaitForHelper(page, cpuMultiplier, networkMultiplier);
681
641
  }
682
642
  waitForEventsAfterAction(action, options) {
683
- const page = this.getSelectedPage();
684
- const cpuMultiplier = this.getCpuThrottlingRate();
685
- const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
686
- const waitForHelper = this.getWaitForHelper(page, cpuMultiplier, networkMultiplier);
643
+ const page = this.#getSelectedMcpPage();
644
+ const cpuMultiplier = page.cpuThrottlingRate;
645
+ const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
646
+ const waitForHelper = this.getWaitForHelper(page.pptrPage, cpuMultiplier, networkMultiplier);
687
647
  return waitForHelper.waitForEventsAfterAction(action, options);
688
648
  }
689
649
  getNetworkRequestStableId(request) {
690
650
  return this.#networkCollector.getIdForResource(request);
691
651
  }
692
- waitForTextOnPage(text, timeout) {
693
- const page = this.getSelectedPage();
652
+ waitForTextOnPage(text, timeout, targetPage) {
653
+ const page = targetPage ?? this.getSelectedPptrPage();
694
654
  const frames = page.frames();
695
655
  let locator = this.#locatorClass.race(frames.flatMap(frame => text.flatMap(value => [
696
656
  frame.locator(`aria/${value}`),
@@ -715,7 +675,7 @@ export class McpContext {
715
675
  },
716
676
  };
717
677
  });
718
- const pages = await this.#getAllPages();
678
+ const { pages } = await this.#getAllPages();
719
679
  await this.#networkCollector.init(pages);
720
680
  }
721
681
  async installExtension(extensionPath) {
@@ -727,6 +687,22 @@ export class McpContext {
727
687
  await this.browser.uninstallExtension(id);
728
688
  this.#extensionRegistry.remove(id);
729
689
  }
690
+ async triggerExtensionAction(id) {
691
+ const page = this.getSelectedPptrPage();
692
+ // @ts-expect-error internal puppeteer api is needed since we don't have a way to get
693
+ // a tab id at the moment
694
+ const theTarget = page._tabId;
695
+ const session = await this.browser.target().createCDPSession();
696
+ try {
697
+ await session.send('Extensions.triggerAction', {
698
+ id,
699
+ targetId: theTarget,
700
+ });
701
+ }
702
+ finally {
703
+ await session.detach();
704
+ }
705
+ }
730
706
  listExtensions() {
731
707
  return this.#extensionRegistry.list();
732
708
  }