chrome-devtools-mcp 0.13.0 → 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 +9 -2
- package/build/src/DevtoolsUtils.js +59 -42
- package/build/src/McpContext.js +92 -13
- package/build/src/McpResponse.js +207 -132
- package/build/src/browser.js +1 -0
- package/build/src/cli.js +9 -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 +6 -0
- package/build/src/logger.js +9 -0
- package/build/src/main.js +13 -3
- package/build/src/telemetry/clearcut-logger.js +83 -12
- package/build/src/telemetry/flag-utils.js +1 -1
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +6 -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 +1252 -205
- 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 +58 -9
- package/build/src/tools/network.js +17 -3
- package/build/src/tools/pages.js +75 -46
- package/build/src/tools/performance.js +6 -20
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/package.json +9 -8
- package/build/src/formatters/consoleFormatter.js +0 -156
- package/build/src/formatters/networkFormatter.js +0 -77
- package/build/src/telemetry/clearcut-sender.js +0 -11
- package/build/src/third_party/devtools.js +0 -6
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
|
{
|
|
@@ -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:**
|
|
@@ -370,7 +373,7 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
370
373
|
<!-- BEGIN AUTO GENERATED OPTIONS -->
|
|
371
374
|
|
|
372
375
|
- **`--autoConnect`/ `--auto-connect`**
|
|
373
|
-
If specified, automatically connects to a browser (Chrome
|
|
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.
|
|
374
377
|
- **Type:** boolean
|
|
375
378
|
- **Default:** `false`
|
|
376
379
|
|
|
@@ -631,6 +634,10 @@ If you hit VM-to-host port forwarding issues, see the “Remote debugging betwee
|
|
|
631
634
|
|
|
632
635
|
For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/).
|
|
633
636
|
|
|
637
|
+
### Debugging Chrome on Android
|
|
638
|
+
|
|
639
|
+
Please consult [these instructions](./docs/debugging-android.md).
|
|
640
|
+
|
|
634
641
|
## Known limitations
|
|
635
642
|
|
|
636
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,10 +52,14 @@ 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;
|
|
59
64
|
#pageIdMap = new WeakMap();
|
|
60
65
|
#nextPageId = 1;
|
|
@@ -62,6 +67,7 @@ export class McpContext {
|
|
|
62
67
|
#traceResults = [];
|
|
63
68
|
#locatorClass;
|
|
64
69
|
#options;
|
|
70
|
+
#uniqueBackendNodeIdToMcpId = new Map();
|
|
65
71
|
constructor(browser, logger, options, locatorClass) {
|
|
66
72
|
this.browser = browser;
|
|
67
73
|
this.logger = logger;
|
|
@@ -88,15 +94,18 @@ export class McpContext {
|
|
|
88
94
|
},
|
|
89
95
|
};
|
|
90
96
|
});
|
|
97
|
+
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
|
|
91
98
|
}
|
|
92
99
|
async #init() {
|
|
93
100
|
const pages = await this.createPagesSnapshot();
|
|
94
101
|
await this.#networkCollector.init(pages);
|
|
95
102
|
await this.#consoleCollector.init(pages);
|
|
103
|
+
await this.#devtoolsUniverseManager.init(pages);
|
|
96
104
|
}
|
|
97
105
|
dispose() {
|
|
98
106
|
this.#networkCollector.dispose();
|
|
99
107
|
this.#consoleCollector.dispose();
|
|
108
|
+
this.#devtoolsUniverseManager.dispose();
|
|
100
109
|
}
|
|
101
110
|
static async from(browser, logger, opts,
|
|
102
111
|
/* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
|
|
@@ -151,14 +160,17 @@ export class McpContext {
|
|
|
151
160
|
const page = this.getSelectedPage();
|
|
152
161
|
return this.#consoleCollector.getData(page, includePreservedMessages);
|
|
153
162
|
}
|
|
163
|
+
getDevToolsUniverse() {
|
|
164
|
+
return this.#devtoolsUniverseManager.get(this.getSelectedPage());
|
|
165
|
+
}
|
|
154
166
|
getConsoleMessageStableId(message) {
|
|
155
167
|
return this.#consoleCollector.getIdForResource(message);
|
|
156
168
|
}
|
|
157
169
|
getConsoleMessageById(id) {
|
|
158
170
|
return this.#consoleCollector.getById(this.getSelectedPage(), id);
|
|
159
171
|
}
|
|
160
|
-
async newPage() {
|
|
161
|
-
const page = await this.browser.newPage();
|
|
172
|
+
async newPage(background) {
|
|
173
|
+
const page = await this.browser.newPage({ background });
|
|
162
174
|
await this.createPagesSnapshot();
|
|
163
175
|
this.selectPage(page);
|
|
164
176
|
this.#networkCollector.addPage(page);
|
|
@@ -211,6 +223,32 @@ export class McpContext {
|
|
|
211
223
|
const page = this.getSelectedPage();
|
|
212
224
|
return this.#geolocationMap.get(page) ?? null;
|
|
213
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
|
+
}
|
|
214
252
|
setIsRunningPerformanceTrace(x) {
|
|
215
253
|
this.#isRunningTrace = x;
|
|
216
254
|
}
|
|
@@ -287,19 +325,23 @@ export class McpContext {
|
|
|
287
325
|
if (!this.#textSnapshot?.idToNode.size) {
|
|
288
326
|
throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
|
|
289
327
|
}
|
|
290
|
-
const [snapshotId] = uid.split('_');
|
|
291
|
-
if (this.#textSnapshot.snapshotId !== snapshotId) {
|
|
292
|
-
throw new Error('This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.');
|
|
293
|
-
}
|
|
294
328
|
const node = this.#textSnapshot?.idToNode.get(uid);
|
|
295
329
|
if (!node) {
|
|
296
|
-
throw new Error('No such element found in the snapshot');
|
|
330
|
+
throw new Error('No such element found in the snapshot.');
|
|
297
331
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
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;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new Error(message, {
|
|
342
|
+
cause: error,
|
|
343
|
+
});
|
|
301
344
|
}
|
|
302
|
-
return handle;
|
|
303
345
|
}
|
|
304
346
|
/**
|
|
305
347
|
* Creates a snapshot of the pages.
|
|
@@ -405,10 +447,24 @@ export class McpContext {
|
|
|
405
447
|
// will be used for the tree serialization and mapping ids back to nodes.
|
|
406
448
|
let idCounter = 0;
|
|
407
449
|
const idToNode = new Map();
|
|
450
|
+
const seenUniqueIds = new Set();
|
|
408
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);
|
|
409
465
|
const nodeWithId = {
|
|
410
466
|
...node,
|
|
411
|
-
id
|
|
467
|
+
id,
|
|
412
468
|
children: node.children
|
|
413
469
|
? node.children.map(child => assignIds(child))
|
|
414
470
|
: [],
|
|
@@ -437,6 +493,12 @@ export class McpContext {
|
|
|
437
493
|
this.#textSnapshot.hasSelectedElement = true;
|
|
438
494
|
this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
|
|
439
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
|
+
}
|
|
440
502
|
}
|
|
441
503
|
getTextSnapshot() {
|
|
442
504
|
return this.#textSnapshot;
|
|
@@ -465,6 +527,8 @@ export class McpContext {
|
|
|
465
527
|
}
|
|
466
528
|
}
|
|
467
529
|
storeTraceRecording(result) {
|
|
530
|
+
// Clear the trace results because we only consume the latest trace currently.
|
|
531
|
+
this.#traceResults = [];
|
|
468
532
|
this.#traceResults.push(result);
|
|
469
533
|
}
|
|
470
534
|
recordedTraces() {
|
|
@@ -511,4 +575,19 @@ export class McpContext {
|
|
|
511
575
|
});
|
|
512
576
|
await this.#networkCollector.init(await this.browser.pages());
|
|
513
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
|
+
}
|
|
514
593
|
}
|