chrome-devtools-mcp 0.12.1 → 0.14.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 +52 -25
- package/build/src/DevtoolsUtils.js +59 -42
- package/build/src/McpContext.js +106 -18
- package/build/src/McpResponse.js +240 -146
- package/build/src/browser.js +4 -2
- package/build/src/cli.js +39 -3
- package/build/src/formatters/ConsoleFormatter.js +153 -0
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/formatters/NetworkFormatter.js +226 -0
- package/build/src/formatters/SnapshotFormatter.js +134 -0
- package/build/src/logger.js +9 -0
- package/build/src/main.js +51 -7
- package/build/src/telemetry/clearcut-logger.js +99 -0
- package/build/src/telemetry/flag-utils.js +45 -0
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +33 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +48 -0
- package/build/src/telemetry/watchdog/main.js +98 -0
- package/build/src/telemetry/watchdog-client.js +51 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +0 -1417
- package/build/src/third_party/devtools-formatter-worker.js +15451 -0
- package/build/src/third_party/index.js +2452 -991
- package/build/src/tools/categories.js +2 -0
- package/build/src/tools/emulation.js +62 -1
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/input.js +88 -12
- package/build/src/tools/network.js +17 -3
- package/build/src/tools/pages.js +117 -53
- package/build/src/tools/performance.js +38 -27
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/build/src/utils/string.js +36 -0
- package/package.json +15 -14
- package/build/src/formatters/consoleFormatter.js +0 -121
- package/build/src/formatters/networkFormatter.js +0 -77
- package/build/src/formatters/snapshotFormatter.js +0 -73
- package/build/src/third_party/devtools.js +0 -6
- package/build/src/third_party/issue-descriptions/SameSiteInvalidSameParty.md +0 -8
- package/build/src/third_party/issue-descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ amp mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
|
|
|
66
66
|
<details>
|
|
67
67
|
<summary>Antigravity</summary>
|
|
68
68
|
|
|
69
|
-
To use the Chrome DevTools MCP server follow the instructions from <a href="https://antigravity.google/docs/mcp">Antigravity's docs
|
|
69
|
+
To use the Chrome DevTools MCP server follow the instructions from <a href="https://antigravity.google/docs/mcp">Antigravity's docs</a> to install a custom MCP server. Add the following config to the MCP servers config:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
{
|
|
@@ -91,10 +91,10 @@ Chrome DevTools MCP will not start the browser instance automatically using this
|
|
|
91
91
|
|
|
92
92
|
<details>
|
|
93
93
|
<summary>Claude Code</summary>
|
|
94
|
-
Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://
|
|
94
|
+
Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://code.claude.com/docs/en/mcp">guide</a>):
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
|
|
97
|
+
claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
</details>
|
|
@@ -205,7 +205,10 @@ Install the Chrome DevTools MCP server using the Gemini CLI.
|
|
|
205
205
|
**Project wide:**
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
|
+
# Either MCP only:
|
|
208
209
|
gemini mcp add chrome-devtools npx chrome-devtools-mcp@latest
|
|
210
|
+
# Or as a Gemini extension (MCP+Skills):
|
|
211
|
+
gemini extensions install --auto-update https://github.com/ChromeDevTools/chrome-devtools-mcp
|
|
209
212
|
```
|
|
210
213
|
|
|
211
214
|
**Globally:**
|
|
@@ -241,6 +244,25 @@ Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Con
|
|
|
241
244
|
|
|
242
245
|
</details>
|
|
243
246
|
|
|
247
|
+
<details>
|
|
248
|
+
<summary>OpenCode</summary>
|
|
249
|
+
|
|
250
|
+
Add the following configuration to your `opencode.json` file. If you don't have one, create it at `~/.config/opencode/opencode.json` (<a href="https://opencode.ai/docs/mcp-servers">guide</a>):
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"$schema": "https://opencode.ai/config.json",
|
|
255
|
+
"mcp": {
|
|
256
|
+
"chrome-devtools": {
|
|
257
|
+
"type": "local",
|
|
258
|
+
"command": ["npx", "-y", "chrome-devtools-mcp@latest"]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
</details>
|
|
265
|
+
|
|
244
266
|
<details>
|
|
245
267
|
<summary>Qoder</summary>
|
|
246
268
|
|
|
@@ -350,20 +372,20 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
350
372
|
|
|
351
373
|
<!-- BEGIN AUTO GENERATED OPTIONS -->
|
|
352
374
|
|
|
353
|
-
- **`--autoConnect`**
|
|
354
|
-
If specified, automatically connects to a browser (Chrome
|
|
375
|
+
- **`--autoConnect`/ `--auto-connect`**
|
|
376
|
+
If specified, automatically connects to a browser (Chrome 144+) running in the user data directory identified by the channel param. Requires the remoted debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.
|
|
355
377
|
- **Type:** boolean
|
|
356
378
|
- **Default:** `false`
|
|
357
379
|
|
|
358
|
-
- **`--browserUrl`, `-u`**
|
|
380
|
+
- **`--browserUrl`/ `--browser-url`, `-u`**
|
|
359
381
|
Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.
|
|
360
382
|
- **Type:** string
|
|
361
383
|
|
|
362
|
-
- **`--wsEndpoint`, `-w`**
|
|
384
|
+
- **`--wsEndpoint`/ `--ws-endpoint`, `-w`**
|
|
363
385
|
WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.
|
|
364
386
|
- **Type:** string
|
|
365
387
|
|
|
366
|
-
- **`--wsHeaders`**
|
|
388
|
+
- **`--wsHeaders`/ `--ws-headers`**
|
|
367
389
|
Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint.
|
|
368
390
|
- **Type:** string
|
|
369
391
|
|
|
@@ -372,7 +394,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
372
394
|
- **Type:** boolean
|
|
373
395
|
- **Default:** `false`
|
|
374
396
|
|
|
375
|
-
- **`--executablePath`, `-e`**
|
|
397
|
+
- **`--executablePath`/ `--executable-path`, `-e`**
|
|
376
398
|
Path to custom Chrome executable.
|
|
377
399
|
- **Type:** string
|
|
378
400
|
|
|
@@ -380,7 +402,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
380
402
|
If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false.
|
|
381
403
|
- **Type:** boolean
|
|
382
404
|
|
|
383
|
-
- **`--userDataDir`**
|
|
405
|
+
- **`--userDataDir`/ `--user-data-dir`**
|
|
384
406
|
Path to the user data directory for Chrome. Default is $HOME/.cache/chrome-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE
|
|
385
407
|
- **Type:** string
|
|
386
408
|
|
|
@@ -389,7 +411,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
389
411
|
- **Type:** string
|
|
390
412
|
- **Choices:** `stable`, `canary`, `beta`, `dev`
|
|
391
413
|
|
|
392
|
-
- **`--logFile`**
|
|
414
|
+
- **`--logFile`/ `--log-file`**
|
|
393
415
|
Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
|
|
394
416
|
- **Type:** string
|
|
395
417
|
|
|
@@ -397,29 +419,33 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
397
419
|
Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
|
|
398
420
|
- **Type:** string
|
|
399
421
|
|
|
400
|
-
- **`--proxyServer`**
|
|
422
|
+
- **`--proxyServer`/ `--proxy-server`**
|
|
401
423
|
Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.
|
|
402
424
|
- **Type:** string
|
|
403
425
|
|
|
404
|
-
- **`--acceptInsecureCerts`**
|
|
426
|
+
- **`--acceptInsecureCerts`/ `--accept-insecure-certs`**
|
|
405
427
|
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
|
|
406
428
|
- **Type:** boolean
|
|
407
429
|
|
|
408
|
-
- **`--chromeArg`**
|
|
430
|
+
- **`--chromeArg`/ `--chrome-arg`**
|
|
409
431
|
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
|
|
410
432
|
- **Type:** array
|
|
411
433
|
|
|
412
|
-
- **`--
|
|
434
|
+
- **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`**
|
|
435
|
+
Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
|
|
436
|
+
- **Type:** array
|
|
437
|
+
|
|
438
|
+
- **`--categoryEmulation`/ `--category-emulation`**
|
|
413
439
|
Set to false to exclude tools related to emulation.
|
|
414
440
|
- **Type:** boolean
|
|
415
441
|
- **Default:** `true`
|
|
416
442
|
|
|
417
|
-
- **`--categoryPerformance`**
|
|
443
|
+
- **`--categoryPerformance`/ `--category-performance`**
|
|
418
444
|
Set to false to exclude tools related to performance.
|
|
419
445
|
- **Type:** boolean
|
|
420
446
|
- **Default:** `true`
|
|
421
447
|
|
|
422
|
-
- **`--categoryNetwork`**
|
|
448
|
+
- **`--categoryNetwork`/ `--category-network`**
|
|
423
449
|
Set to false to exclude tools related to network.
|
|
424
450
|
- **Type:** boolean
|
|
425
451
|
- **Default:** `true`
|
|
@@ -499,7 +525,7 @@ In these cases, start Chrome first and let the Chrome DevTools MCP server connec
|
|
|
499
525
|
|
|
500
526
|
**Step 1:** Set up remote debugging in Chrome
|
|
501
527
|
|
|
502
|
-
In Chrome, do the following to set up remote debugging:
|
|
528
|
+
In Chrome (\>= M144), do the following to set up remote debugging:
|
|
503
529
|
|
|
504
530
|
1. Navigate to `chrome://inspect/#remote-debugging` to enable remote debugging.
|
|
505
531
|
2. Follow the dialog UI to allow or disallow incoming debugging connections.
|
|
@@ -516,17 +542,13 @@ The following code snippet is an example configuration for gemini-cli:
|
|
|
516
542
|
"mcpServers": {
|
|
517
543
|
"chrome-devtools": {
|
|
518
544
|
"command": "npx",
|
|
519
|
-
"args": [
|
|
520
|
-
"chrome-devtools-mcp@latest",
|
|
521
|
-
"--autoConnect",
|
|
522
|
-
"--channel=canary"
|
|
523
|
-
]
|
|
545
|
+
"args": ["chrome-devtools-mcp@latest", "--autoConnect", "--channel=beta"]
|
|
524
546
|
}
|
|
525
547
|
}
|
|
526
548
|
}
|
|
527
549
|
```
|
|
528
550
|
|
|
529
|
-
Note: you have to specify `--channel=
|
|
551
|
+
Note: you have to specify `--channel=beta` until Chrome M144 has reached the
|
|
530
552
|
stable channel.
|
|
531
553
|
|
|
532
554
|
**Step 3:** Test your setup
|
|
@@ -537,7 +559,8 @@ Make sure your browser is running. Open gemini-cli and run the following prompt:
|
|
|
537
559
|
Check the performance of https://developers.chrome.com
|
|
538
560
|
```
|
|
539
561
|
|
|
540
|
-
|
|
562
|
+
> [!NOTE]
|
|
563
|
+
> The <code>autoConnect</code> option requires the user to start Chrome. If the user has multiple active profiles, the MCP server will connect to the default profile (as determined by Chrome). The MCP server has access to all open windows for the selected profile.
|
|
541
564
|
|
|
542
565
|
The Chrome DevTools MCP server will try to connect to your running Chrome
|
|
543
566
|
instance. It shows a dialog asking for user permission.
|
|
@@ -611,6 +634,10 @@ If you hit VM-to-host port forwarding issues, see the “Remote debugging betwee
|
|
|
611
634
|
|
|
612
635
|
For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/).
|
|
613
636
|
|
|
637
|
+
### Debugging Chrome on Android
|
|
638
|
+
|
|
639
|
+
Please consult [these instructions](./docs/debugging-android.md).
|
|
640
|
+
|
|
614
641
|
## Known limitations
|
|
615
642
|
|
|
616
643
|
### Operating system sandboxes
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { PuppeteerDevToolsConnection } from './DevToolsConnectionAdapter.js';
|
|
7
|
-
import { ISSUE_UTILS } from './issue-descriptions.js';
|
|
8
|
-
import { logger } from './logger.js';
|
|
9
7
|
import { Mutex } from './Mutex.js';
|
|
10
8
|
import { DevTools } from './third_party/index.js';
|
|
11
9
|
export function extractUrlLikeFromDevToolsTitle(title) {
|
|
@@ -64,46 +62,6 @@ export class FakeIssuesManager extends DevTools.Common.ObjectWrapper
|
|
|
64
62
|
return [];
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
export function mapIssueToMessageObject(issue) {
|
|
68
|
-
const count = issue.getAggregatedIssuesCount();
|
|
69
|
-
const markdownDescription = issue.getDescription();
|
|
70
|
-
const filename = markdownDescription?.file;
|
|
71
|
-
if (!markdownDescription) {
|
|
72
|
-
logger(`no description found for issue:` + issue.code);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
const rawMarkdown = filename
|
|
76
|
-
? ISSUE_UTILS.getIssueDescription(filename)
|
|
77
|
-
: null;
|
|
78
|
-
if (!rawMarkdown) {
|
|
79
|
-
logger(`no markdown ${filename} found for issue:` + issue.code);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
let processedMarkdown;
|
|
83
|
-
let title;
|
|
84
|
-
try {
|
|
85
|
-
processedMarkdown =
|
|
86
|
-
DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription.substitutions);
|
|
87
|
-
const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
|
|
88
|
-
title =
|
|
89
|
-
DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
logger('error parsing markdown for issue ' + issue.code());
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
if (!title) {
|
|
96
|
-
logger('cannot read issue title from ' + filename);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
type: 'issue',
|
|
101
|
-
item: issue,
|
|
102
|
-
message: title,
|
|
103
|
-
count,
|
|
104
|
-
description: processedMarkdown,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
65
|
// DevTools CDP errors can get noisy.
|
|
108
66
|
DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
|
|
109
67
|
DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
|
|
@@ -115,6 +73,11 @@ DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
|
|
|
115
73
|
},
|
|
116
74
|
});
|
|
117
75
|
DevTools.I18n.i18n.registerLocaleDataForTest('en-US', {});
|
|
76
|
+
DevTools.Formatter.FormatterWorkerPool.FormatterWorkerPool.instance({
|
|
77
|
+
forceNew: true,
|
|
78
|
+
entrypointURL: import.meta
|
|
79
|
+
.resolve('./third_party/devtools-formatter-worker.js'),
|
|
80
|
+
});
|
|
118
81
|
export class UniverseManager {
|
|
119
82
|
#browser;
|
|
120
83
|
#createUniverseFor;
|
|
@@ -206,3 +169,57 @@ const SKIP_ALL_PAUSES = {
|
|
|
206
169
|
// Do nothing.
|
|
207
170
|
},
|
|
208
171
|
};
|
|
172
|
+
export async function createStackTraceForConsoleMessage(devTools, consoleMessage) {
|
|
173
|
+
const message = consoleMessage;
|
|
174
|
+
const rawStackTrace = message._rawStackTrace();
|
|
175
|
+
if (!rawStackTrace) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const targetManager = devTools.universe.context.get(DevTools.TargetManager);
|
|
179
|
+
const messageTargetId = message._targetId();
|
|
180
|
+
const target = messageTargetId
|
|
181
|
+
? targetManager.targetById(messageTargetId) || devTools.target
|
|
182
|
+
: devTools.target;
|
|
183
|
+
const model = target.model(DevTools.DebuggerModel);
|
|
184
|
+
// DevTools doesn't wait for source maps to attach before building a stack trace, rather it'll send
|
|
185
|
+
// an update event once a source map was attached and the stack trace retranslated. This doesn't
|
|
186
|
+
// work in the MCP case, so we'll collect all script IDs upfront and wait for any pending source map
|
|
187
|
+
// loads before creating the stack trace. We might also have to wait for Debugger.ScriptParsed events if
|
|
188
|
+
// the stack trace is created particularly early.
|
|
189
|
+
const scriptIds = new Set();
|
|
190
|
+
for (const frame of rawStackTrace.callFrames) {
|
|
191
|
+
scriptIds.add(frame.scriptId);
|
|
192
|
+
}
|
|
193
|
+
for (let asyncStack = rawStackTrace.parent; asyncStack; asyncStack = asyncStack.parent) {
|
|
194
|
+
for (const frame of asyncStack.callFrames) {
|
|
195
|
+
scriptIds.add(frame.scriptId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const signal = AbortSignal.timeout(1_000);
|
|
199
|
+
await Promise.all([...scriptIds].map(id => waitForScript(model, id, signal)
|
|
200
|
+
.then(script => model.sourceMapManager().sourceMapForClientPromise(script))
|
|
201
|
+
.catch()));
|
|
202
|
+
const binding = devTools.universe.context.get(DevTools.DebuggerWorkspaceBinding);
|
|
203
|
+
// DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe.
|
|
204
|
+
return binding.createStackTraceFromProtocolRuntime(rawStackTrace, target);
|
|
205
|
+
}
|
|
206
|
+
// Waits indefinitely for the script so pair it with Promise.race.
|
|
207
|
+
async function waitForScript(model, scriptId, signal) {
|
|
208
|
+
while (true) {
|
|
209
|
+
if (signal.aborted) {
|
|
210
|
+
throw signal.reason;
|
|
211
|
+
}
|
|
212
|
+
const script = model.scriptForId(scriptId);
|
|
213
|
+
if (script) {
|
|
214
|
+
return script;
|
|
215
|
+
}
|
|
216
|
+
await new Promise((resolve, reject) => {
|
|
217
|
+
signal.addEventListener('abort', () => reject(signal.reason), {
|
|
218
|
+
once: true,
|
|
219
|
+
});
|
|
220
|
+
void model
|
|
221
|
+
.once('ParsedScriptSource')
|
|
222
|
+
.then(resolve);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { extractUrlLikeFromDevToolsTitle, urlsEqual } from './DevtoolsUtils.js';
|
|
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
12
|
import { listPages } from './tools/pages.js';
|
|
13
13
|
import { takeSnapshot } from './tools/snapshot.js';
|
|
14
14
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
15
|
+
import { ExtensionRegistry, } from './utils/ExtensionRegistry.js';
|
|
15
16
|
import { WaitForHelper } from './WaitForHelper.js';
|
|
16
17
|
const DEFAULT_TIMEOUT = 5_000;
|
|
17
18
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
@@ -51,15 +52,22 @@ export class McpContext {
|
|
|
51
52
|
#textSnapshot = null;
|
|
52
53
|
#networkCollector;
|
|
53
54
|
#consoleCollector;
|
|
55
|
+
#devtoolsUniverseManager;
|
|
56
|
+
#extensionRegistry = new ExtensionRegistry();
|
|
54
57
|
#isRunningTrace = false;
|
|
55
58
|
#networkConditionsMap = new WeakMap();
|
|
56
59
|
#cpuThrottlingRateMap = new WeakMap();
|
|
57
60
|
#geolocationMap = new WeakMap();
|
|
61
|
+
#viewportMap = new WeakMap();
|
|
62
|
+
#userAgentMap = new WeakMap();
|
|
58
63
|
#dialog;
|
|
64
|
+
#pageIdMap = new WeakMap();
|
|
65
|
+
#nextPageId = 1;
|
|
59
66
|
#nextSnapshotId = 1;
|
|
60
67
|
#traceResults = [];
|
|
61
68
|
#locatorClass;
|
|
62
69
|
#options;
|
|
70
|
+
#uniqueBackendNodeIdToMcpId = new Map();
|
|
63
71
|
constructor(browser, logger, options, locatorClass) {
|
|
64
72
|
this.browser = browser;
|
|
65
73
|
this.logger = logger;
|
|
@@ -86,15 +94,18 @@ export class McpContext {
|
|
|
86
94
|
},
|
|
87
95
|
};
|
|
88
96
|
});
|
|
97
|
+
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
|
|
89
98
|
}
|
|
90
99
|
async #init() {
|
|
91
100
|
const pages = await this.createPagesSnapshot();
|
|
92
101
|
await this.#networkCollector.init(pages);
|
|
93
102
|
await this.#consoleCollector.init(pages);
|
|
103
|
+
await this.#devtoolsUniverseManager.init(pages);
|
|
94
104
|
}
|
|
95
105
|
dispose() {
|
|
96
106
|
this.#networkCollector.dispose();
|
|
97
107
|
this.#consoleCollector.dispose();
|
|
108
|
+
this.#devtoolsUniverseManager.dispose();
|
|
98
109
|
}
|
|
99
110
|
static async from(browser, logger, opts,
|
|
100
111
|
/* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
|
|
@@ -149,25 +160,28 @@ export class McpContext {
|
|
|
149
160
|
const page = this.getSelectedPage();
|
|
150
161
|
return this.#consoleCollector.getData(page, includePreservedMessages);
|
|
151
162
|
}
|
|
163
|
+
getDevToolsUniverse() {
|
|
164
|
+
return this.#devtoolsUniverseManager.get(this.getSelectedPage());
|
|
165
|
+
}
|
|
152
166
|
getConsoleMessageStableId(message) {
|
|
153
167
|
return this.#consoleCollector.getIdForResource(message);
|
|
154
168
|
}
|
|
155
169
|
getConsoleMessageById(id) {
|
|
156
170
|
return this.#consoleCollector.getById(this.getSelectedPage(), id);
|
|
157
171
|
}
|
|
158
|
-
async newPage() {
|
|
159
|
-
const page = await this.browser.newPage();
|
|
172
|
+
async newPage(background) {
|
|
173
|
+
const page = await this.browser.newPage({ background });
|
|
160
174
|
await this.createPagesSnapshot();
|
|
161
175
|
this.selectPage(page);
|
|
162
176
|
this.#networkCollector.addPage(page);
|
|
163
177
|
this.#consoleCollector.addPage(page);
|
|
164
178
|
return page;
|
|
165
179
|
}
|
|
166
|
-
async closePage(
|
|
180
|
+
async closePage(pageId) {
|
|
167
181
|
if (this.#pages.length === 1) {
|
|
168
182
|
throw new Error(CLOSE_PAGE_ERROR);
|
|
169
183
|
}
|
|
170
|
-
const page = this.
|
|
184
|
+
const page = this.getPageById(pageId);
|
|
171
185
|
await page.close({ runBeforeUnload: false });
|
|
172
186
|
}
|
|
173
187
|
getNetworkRequestById(reqid) {
|
|
@@ -209,6 +223,32 @@ export class McpContext {
|
|
|
209
223
|
const page = this.getSelectedPage();
|
|
210
224
|
return this.#geolocationMap.get(page) ?? null;
|
|
211
225
|
}
|
|
226
|
+
setViewport(viewport) {
|
|
227
|
+
const page = this.getSelectedPage();
|
|
228
|
+
if (viewport === null) {
|
|
229
|
+
this.#viewportMap.delete(page);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.#viewportMap.set(page, viewport);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
getViewport() {
|
|
236
|
+
const page = this.getSelectedPage();
|
|
237
|
+
return this.#viewportMap.get(page) ?? null;
|
|
238
|
+
}
|
|
239
|
+
setUserAgent(userAgent) {
|
|
240
|
+
const page = this.getSelectedPage();
|
|
241
|
+
if (userAgent === null) {
|
|
242
|
+
this.#userAgentMap.delete(page);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.#userAgentMap.set(page, userAgent);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
getUserAgent() {
|
|
249
|
+
const page = this.getSelectedPage();
|
|
250
|
+
return this.#userAgentMap.get(page) ?? null;
|
|
251
|
+
}
|
|
212
252
|
setIsRunningPerformanceTrace(x) {
|
|
213
253
|
this.#isRunningTrace = x;
|
|
214
254
|
}
|
|
@@ -231,14 +271,16 @@ export class McpContext {
|
|
|
231
271
|
}
|
|
232
272
|
return page;
|
|
233
273
|
}
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const page = pages[idx];
|
|
274
|
+
getPageById(pageId) {
|
|
275
|
+
const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
|
|
237
276
|
if (!page) {
|
|
238
277
|
throw new Error('No page found');
|
|
239
278
|
}
|
|
240
279
|
return page;
|
|
241
280
|
}
|
|
281
|
+
getPageId(page) {
|
|
282
|
+
return this.#pageIdMap.get(page);
|
|
283
|
+
}
|
|
242
284
|
#dialogHandler = (dialog) => {
|
|
243
285
|
this.#dialog = dialog;
|
|
244
286
|
};
|
|
@@ -283,25 +325,34 @@ export class McpContext {
|
|
|
283
325
|
if (!this.#textSnapshot?.idToNode.size) {
|
|
284
326
|
throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
|
|
285
327
|
}
|
|
286
|
-
const [snapshotId] = uid.split('_');
|
|
287
|
-
if (this.#textSnapshot.snapshotId !== snapshotId) {
|
|
288
|
-
throw new Error('This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.');
|
|
289
|
-
}
|
|
290
328
|
const node = this.#textSnapshot?.idToNode.get(uid);
|
|
291
329
|
if (!node) {
|
|
292
|
-
throw new Error('No such element found in the snapshot');
|
|
330
|
+
throw new Error('No such element found in the snapshot.');
|
|
331
|
+
}
|
|
332
|
+
const message = `Element with uid ${uid} no longer exists on the page.`;
|
|
333
|
+
try {
|
|
334
|
+
const handle = await node.elementHandle();
|
|
335
|
+
if (!handle) {
|
|
336
|
+
throw new Error(message);
|
|
337
|
+
}
|
|
338
|
+
return handle;
|
|
293
339
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new Error(message, {
|
|
342
|
+
cause: error,
|
|
343
|
+
});
|
|
297
344
|
}
|
|
298
|
-
return handle;
|
|
299
345
|
}
|
|
300
346
|
/**
|
|
301
347
|
* Creates a snapshot of the pages.
|
|
302
348
|
*/
|
|
303
349
|
async createPagesSnapshot() {
|
|
304
350
|
const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
351
|
+
for (const page of allPages) {
|
|
352
|
+
if (!this.#pageIdMap.has(page)) {
|
|
353
|
+
this.#pageIdMap.set(page, this.#nextPageId++);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
305
356
|
this.#pages = allPages.filter(page => {
|
|
306
357
|
// If we allow debugging DevTools windows, return all pages.
|
|
307
358
|
// If we are in regular mode, the user should only see non-DevTools page.
|
|
@@ -396,10 +447,24 @@ export class McpContext {
|
|
|
396
447
|
// will be used for the tree serialization and mapping ids back to nodes.
|
|
397
448
|
let idCounter = 0;
|
|
398
449
|
const idToNode = new Map();
|
|
450
|
+
const seenUniqueIds = new Set();
|
|
399
451
|
const assignIds = (node) => {
|
|
452
|
+
let id = '';
|
|
453
|
+
// @ts-expect-error untyped loaderId & backendNodeId.
|
|
454
|
+
const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
|
|
455
|
+
if (this.#uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
|
|
456
|
+
// Re-use MCP exposed ID if the uniqueId is the same.
|
|
457
|
+
id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Only generate a new ID if we have not seen the node before.
|
|
461
|
+
id = `${snapshotId}_${idCounter++}`;
|
|
462
|
+
this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
|
|
463
|
+
}
|
|
464
|
+
seenUniqueIds.add(uniqueBackendId);
|
|
400
465
|
const nodeWithId = {
|
|
401
466
|
...node,
|
|
402
|
-
id
|
|
467
|
+
id,
|
|
403
468
|
children: node.children
|
|
404
469
|
? node.children.map(child => assignIds(child))
|
|
405
470
|
: [],
|
|
@@ -428,6 +493,12 @@ export class McpContext {
|
|
|
428
493
|
this.#textSnapshot.hasSelectedElement = true;
|
|
429
494
|
this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
|
|
430
495
|
}
|
|
496
|
+
// Clean up unique IDs that we did not see anymore.
|
|
497
|
+
for (const key of this.#uniqueBackendNodeIdToMcpId.keys()) {
|
|
498
|
+
if (!seenUniqueIds.has(key)) {
|
|
499
|
+
this.#uniqueBackendNodeIdToMcpId.delete(key);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
431
502
|
}
|
|
432
503
|
getTextSnapshot() {
|
|
433
504
|
return this.#textSnapshot;
|
|
@@ -456,6 +527,8 @@ export class McpContext {
|
|
|
456
527
|
}
|
|
457
528
|
}
|
|
458
529
|
storeTraceRecording(result) {
|
|
530
|
+
// Clear the trace results because we only consume the latest trace currently.
|
|
531
|
+
this.#traceResults = [];
|
|
459
532
|
this.#traceResults.push(result);
|
|
460
533
|
}
|
|
461
534
|
recordedTraces() {
|
|
@@ -502,4 +575,19 @@ export class McpContext {
|
|
|
502
575
|
});
|
|
503
576
|
await this.#networkCollector.init(await this.browser.pages());
|
|
504
577
|
}
|
|
578
|
+
async installExtension(extensionPath) {
|
|
579
|
+
const id = await this.browser.installExtension(extensionPath);
|
|
580
|
+
await this.#extensionRegistry.registerExtension(id, extensionPath);
|
|
581
|
+
return id;
|
|
582
|
+
}
|
|
583
|
+
async uninstallExtension(id) {
|
|
584
|
+
await this.browser.uninstallExtension(id);
|
|
585
|
+
this.#extensionRegistry.remove(id);
|
|
586
|
+
}
|
|
587
|
+
listExtensions() {
|
|
588
|
+
return this.#extensionRegistry.list();
|
|
589
|
+
}
|
|
590
|
+
getExtension(id) {
|
|
591
|
+
return this.#extensionRegistry.getById(id);
|
|
592
|
+
}
|
|
505
593
|
}
|