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.
- package/README.md +6 -5
- package/build/src/McpContext.js +242 -266
- package/build/src/McpPage.js +95 -0
- package/build/src/McpResponse.js +124 -48
- package/build/src/bin/chrome-devtools-cli-options.js +651 -0
- package/build/src/{cli.js → bin/chrome-devtools-mcp-cli-options.js} +12 -2
- package/build/src/bin/chrome-devtools-mcp-main.js +35 -0
- package/build/src/bin/chrome-devtools-mcp.js +21 -0
- package/build/src/bin/chrome-devtools.js +185 -0
- package/build/src/bin/cliDefinitions.js +615 -0
- package/build/src/browser.js +13 -12
- package/build/src/daemon/client.js +152 -0
- package/build/src/daemon/daemon.js +56 -17
- package/build/src/daemon/types.js +6 -0
- package/build/src/daemon/utils.js +57 -16
- package/build/src/index.js +204 -16
- package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
- package/build/src/third_party/bundled-packages.json +4 -3
- package/build/src/third_party/devtools-formatter-worker.js +5 -14
- package/build/src/third_party/index.js +2128 -472
- package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +7 -0
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
- package/build/src/tools/ToolDefinition.js +52 -0
- package/build/src/tools/console.js +3 -3
- package/build/src/tools/emulation.js +13 -45
- package/build/src/tools/extensions.js +17 -0
- package/build/src/tools/input.js +33 -33
- package/build/src/tools/lighthouse.js +123 -0
- package/build/src/tools/memory.js +5 -5
- package/build/src/tools/network.js +7 -7
- package/build/src/tools/pages.js +32 -32
- package/build/src/tools/performance.js +16 -14
- package/build/src/tools/screencast.js +5 -5
- package/build/src/tools/screenshot.js +6 -6
- package/build/src/tools/script.js +99 -49
- package/build/src/tools/slim/tools.js +18 -18
- package/build/src/tools/snapshot.js +5 -4
- package/build/src/tools/tools.js +2 -0
- package/build/src/types.js +6 -0
- package/build/src/utils/files.js +19 -0
- package/build/src/version.js +1 -1
- package/package.json +15 -9
- package/build/src/main.js +0 -203
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
-
#
|
|
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(
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 (
|
|
297
|
-
|
|
235
|
+
if (!options.userAgent) {
|
|
236
|
+
await page.setUserAgent({ userAgent: undefined });
|
|
237
|
+
delete newSettings.userAgent;
|
|
298
238
|
}
|
|
299
239
|
else {
|
|
300
|
-
|
|
240
|
+
await page.setUserAgent({ userAgent: options.userAgent });
|
|
241
|
+
newSettings.userAgent = options.userAgent;
|
|
301
242
|
}
|
|
302
|
-
if (
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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 ||
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
513
|
+
return this.#mcpPages.get(page)?.isolatedContextName;
|
|
543
514
|
}
|
|
544
515
|
getDevToolsPage(page) {
|
|
545
|
-
return this.#
|
|
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
|
|
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
|
|
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 (
|
|
567
|
+
if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
|
|
598
568
|
// Re-use MCP exposed ID if the uniqueId is the same.
|
|
599
|
-
id =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
+
page.textSnapshot = snapshot;
|
|
604
|
+
const data = devtoolsData ?? (await this.getDevToolsData(page));
|
|
634
605
|
if (data?.cdpBackendNodeId) {
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
610
|
+
for (const key of uniqueBackendNodeIdToMcpId.keys()) {
|
|
640
611
|
if (!seenUniqueIds.has(key)) {
|
|
641
|
-
|
|
612
|
+
uniqueBackendNodeIdToMcpId.delete(key);
|
|
642
613
|
}
|
|
643
614
|
}
|
|
644
615
|
}
|
|
645
|
-
|
|
646
|
-
return
|
|
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
|
|
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
|
|
684
|
-
const cpuMultiplier =
|
|
685
|
-
const networkMultiplier = getNetworkMultiplierFromString(
|
|
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.
|
|
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
|
}
|