chrome-devtools-mcp 0.13.0 → 0.15.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 (37) hide show
  1. package/README.md +28 -2
  2. package/build/src/DevtoolsUtils.js +59 -42
  3. package/build/src/McpContext.js +106 -13
  4. package/build/src/McpResponse.js +213 -132
  5. package/build/src/browser.js +1 -0
  6. package/build/src/cli.js +29 -6
  7. package/build/src/formatters/ConsoleFormatter.js +153 -0
  8. package/build/src/formatters/IssueFormatter.js +190 -0
  9. package/build/src/formatters/NetworkFormatter.js +226 -0
  10. package/build/src/formatters/SnapshotFormatter.js +6 -0
  11. package/build/src/logger.js +9 -0
  12. package/build/src/main.js +16 -3
  13. package/build/src/telemetry/clearcut-logger.js +86 -12
  14. package/build/src/telemetry/flag-utils.js +1 -1
  15. package/build/src/telemetry/metric-utils.js +14 -0
  16. package/build/src/telemetry/persistence.js +53 -0
  17. package/build/src/telemetry/types.js +6 -0
  18. package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
  19. package/build/src/telemetry/watchdog/main.js +127 -0
  20. package/build/src/telemetry/watchdog-client.js +60 -0
  21. package/build/src/third_party/THIRD_PARTY_NOTICES +6 -5
  22. package/build/src/third_party/devtools-formatter-worker.js +15451 -0
  23. package/build/src/third_party/index.js +1356 -282
  24. package/build/src/tools/categories.js +2 -0
  25. package/build/src/tools/emulation.js +83 -1
  26. package/build/src/tools/extensions.js +79 -0
  27. package/build/src/tools/input.js +58 -9
  28. package/build/src/tools/network.js +17 -3
  29. package/build/src/tools/pages.js +91 -46
  30. package/build/src/tools/performance.js +6 -20
  31. package/build/src/tools/tools.js +2 -0
  32. package/build/src/utils/ExtensionRegistry.js +35 -0
  33. package/package.json +9 -8
  34. package/build/src/formatters/consoleFormatter.js +0 -156
  35. package/build/src/formatters/networkFormatter.js +0 -77
  36. package/build/src/telemetry/clearcut-sender.js +0 -11
  37. package/build/src/third_party/devtools.js +0 -6
package/README.md CHANGED
@@ -27,6 +27,20 @@ allowing them to inspect, debug, and modify any data in the browser or DevTools.
27
27
  Avoid sharing sensitive or personal information that you don't want to share with
28
28
  MCP clients.
29
29
 
30
+ ## **Usage statistics**
31
+
32
+ Google collects usage statistics (such as tool invocation success rates, latency, and environment information) to improve the reliability and performance of Chrome DevTools MCP.
33
+
34
+ Data collection is **enabled by default**. You can opt-out by passing the `--no-usage-statistics` flag when starting the server:
35
+
36
+ ```json
37
+ "args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"]
38
+ ```
39
+
40
+ Google handles this data in accordance with the [Google Privacy Policy](https://policies.google.com/privacy).
41
+
42
+ Google's collection of usage statistics for Chrome DevTools MCP is independent from the Chrome browser's usage statistics. Opting out of Chrome metrics does not automatically opt you out of this tool, and vice-versa.
43
+
30
44
  ## Requirements
31
45
 
32
46
  - [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
@@ -66,7 +80,7 @@ amp mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
66
80
  <details>
67
81
  <summary>Antigravity</summary>
68
82
 
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:
83
+ 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
84
 
71
85
  ```bash
72
86
  {
@@ -205,7 +219,10 @@ Install the Chrome DevTools MCP server using the Gemini CLI.
205
219
  **Project wide:**
206
220
 
207
221
  ```bash
222
+ # Either MCP only:
208
223
  gemini mcp add chrome-devtools npx chrome-devtools-mcp@latest
224
+ # Or as a Gemini extension (MCP+Skills):
225
+ gemini extensions install --auto-update https://github.com/ChromeDevTools/chrome-devtools-mcp
209
226
  ```
210
227
 
211
228
  **Globally:**
@@ -370,7 +387,7 @@ The Chrome DevTools MCP server supports the following configuration option:
370
387
  <!-- BEGIN AUTO GENERATED OPTIONS -->
371
388
 
372
389
  - **`--autoConnect`/ `--auto-connect`**
373
- If specified, automatically connects to a browser (Chrome 145+) running in the user data directory identified by the channel param. Requires remote debugging being enabled in Chrome here: chrome://inspect/#remote-debugging.
390
+ 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
391
  - **Type:** boolean
375
392
  - **Default:** `false`
376
393
 
@@ -447,6 +464,11 @@ The Chrome DevTools MCP server supports the following configuration option:
447
464
  - **Type:** boolean
448
465
  - **Default:** `true`
449
466
 
467
+ - **`--usageStatistics`/ `--usage-statistics`**
468
+ Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics.
469
+ - **Type:** boolean
470
+ - **Default:** `true`
471
+
450
472
  <!-- END AUTO GENERATED OPTIONS -->
451
473
 
452
474
  Pass them via the `args` property in the JSON configuration. For example:
@@ -631,6 +653,10 @@ If you hit VM-to-host port forwarding issues, see the “Remote debugging betwee
631
653
 
632
654
  For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/).
633
655
 
656
+ ### Debugging Chrome on Android
657
+
658
+ Please consult [these instructions](./docs/debugging-android.md).
659
+
634
660
  ## Known limitations
635
661
 
636
662
  ### 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
+ }
@@ -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,15 @@ 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();
63
+ #colorSchemeMap = new WeakMap();
58
64
  #dialog;
59
65
  #pageIdMap = new WeakMap();
60
66
  #nextPageId = 1;
@@ -62,6 +68,7 @@ export class McpContext {
62
68
  #traceResults = [];
63
69
  #locatorClass;
64
70
  #options;
71
+ #uniqueBackendNodeIdToMcpId = new Map();
65
72
  constructor(browser, logger, options, locatorClass) {
66
73
  this.browser = browser;
67
74
  this.logger = logger;
@@ -88,15 +95,18 @@ export class McpContext {
88
95
  },
89
96
  };
90
97
  });
98
+ this.#devtoolsUniverseManager = new UniverseManager(this.browser);
91
99
  }
92
100
  async #init() {
93
101
  const pages = await this.createPagesSnapshot();
94
102
  await this.#networkCollector.init(pages);
95
103
  await this.#consoleCollector.init(pages);
104
+ await this.#devtoolsUniverseManager.init(pages);
96
105
  }
97
106
  dispose() {
98
107
  this.#networkCollector.dispose();
99
108
  this.#consoleCollector.dispose();
109
+ this.#devtoolsUniverseManager.dispose();
100
110
  }
101
111
  static async from(browser, logger, opts,
102
112
  /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
@@ -151,14 +161,17 @@ export class McpContext {
151
161
  const page = this.getSelectedPage();
152
162
  return this.#consoleCollector.getData(page, includePreservedMessages);
153
163
  }
164
+ getDevToolsUniverse() {
165
+ return this.#devtoolsUniverseManager.get(this.getSelectedPage());
166
+ }
154
167
  getConsoleMessageStableId(message) {
155
168
  return this.#consoleCollector.getIdForResource(message);
156
169
  }
157
170
  getConsoleMessageById(id) {
158
171
  return this.#consoleCollector.getById(this.getSelectedPage(), id);
159
172
  }
160
- async newPage() {
161
- const page = await this.browser.newPage();
173
+ async newPage(background) {
174
+ const page = await this.browser.newPage({ background });
162
175
  await this.createPagesSnapshot();
163
176
  this.selectPage(page);
164
177
  this.#networkCollector.addPage(page);
@@ -211,6 +224,45 @@ export class McpContext {
211
224
  const page = this.getSelectedPage();
212
225
  return this.#geolocationMap.get(page) ?? null;
213
226
  }
227
+ setViewport(viewport) {
228
+ const page = this.getSelectedPage();
229
+ if (viewport === null) {
230
+ this.#viewportMap.delete(page);
231
+ }
232
+ else {
233
+ this.#viewportMap.set(page, viewport);
234
+ }
235
+ }
236
+ getViewport() {
237
+ const page = this.getSelectedPage();
238
+ return this.#viewportMap.get(page) ?? null;
239
+ }
240
+ setUserAgent(userAgent) {
241
+ const page = this.getSelectedPage();
242
+ if (userAgent === null) {
243
+ this.#userAgentMap.delete(page);
244
+ }
245
+ else {
246
+ this.#userAgentMap.set(page, userAgent);
247
+ }
248
+ }
249
+ getUserAgent() {
250
+ const page = this.getSelectedPage();
251
+ return this.#userAgentMap.get(page) ?? null;
252
+ }
253
+ setColorScheme(scheme) {
254
+ const page = this.getSelectedPage();
255
+ if (scheme === null) {
256
+ this.#colorSchemeMap.delete(page);
257
+ }
258
+ else {
259
+ this.#colorSchemeMap.set(page, scheme);
260
+ }
261
+ }
262
+ getColorScheme() {
263
+ const page = this.getSelectedPage();
264
+ return this.#colorSchemeMap.get(page) ?? null;
265
+ }
214
266
  setIsRunningPerformanceTrace(x) {
215
267
  this.#isRunningTrace = x;
216
268
  }
@@ -287,19 +339,23 @@ export class McpContext {
287
339
  if (!this.#textSnapshot?.idToNode.size) {
288
340
  throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
289
341
  }
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
342
  const node = this.#textSnapshot?.idToNode.get(uid);
295
343
  if (!node) {
296
- throw new Error('No such element found in the snapshot');
344
+ throw new Error('No such element found in the snapshot.');
297
345
  }
298
- const handle = await node.elementHandle();
299
- if (!handle) {
300
- throw new Error('No such element found in the snapshot');
346
+ const message = `Element with uid ${uid} no longer exists on the page.`;
347
+ try {
348
+ const handle = await node.elementHandle();
349
+ if (!handle) {
350
+ throw new Error(message);
351
+ }
352
+ return handle;
353
+ }
354
+ catch (error) {
355
+ throw new Error(message, {
356
+ cause: error,
357
+ });
301
358
  }
302
- return handle;
303
359
  }
304
360
  /**
305
361
  * Creates a snapshot of the pages.
@@ -405,10 +461,24 @@ export class McpContext {
405
461
  // will be used for the tree serialization and mapping ids back to nodes.
406
462
  let idCounter = 0;
407
463
  const idToNode = new Map();
464
+ const seenUniqueIds = new Set();
408
465
  const assignIds = (node) => {
466
+ let id = '';
467
+ // @ts-expect-error untyped loaderId & backendNodeId.
468
+ const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
469
+ if (this.#uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
470
+ // Re-use MCP exposed ID if the uniqueId is the same.
471
+ id = this.#uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
472
+ }
473
+ else {
474
+ // Only generate a new ID if we have not seen the node before.
475
+ id = `${snapshotId}_${idCounter++}`;
476
+ this.#uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
477
+ }
478
+ seenUniqueIds.add(uniqueBackendId);
409
479
  const nodeWithId = {
410
480
  ...node,
411
- id: `${snapshotId}_${idCounter++}`,
481
+ id,
412
482
  children: node.children
413
483
  ? node.children.map(child => assignIds(child))
414
484
  : [],
@@ -437,6 +507,12 @@ export class McpContext {
437
507
  this.#textSnapshot.hasSelectedElement = true;
438
508
  this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
439
509
  }
510
+ // Clean up unique IDs that we did not see anymore.
511
+ for (const key of this.#uniqueBackendNodeIdToMcpId.keys()) {
512
+ if (!seenUniqueIds.has(key)) {
513
+ this.#uniqueBackendNodeIdToMcpId.delete(key);
514
+ }
515
+ }
440
516
  }
441
517
  getTextSnapshot() {
442
518
  return this.#textSnapshot;
@@ -465,6 +541,8 @@ export class McpContext {
465
541
  }
466
542
  }
467
543
  storeTraceRecording(result) {
544
+ // Clear the trace results because we only consume the latest trace currently.
545
+ this.#traceResults = [];
468
546
  this.#traceResults.push(result);
469
547
  }
470
548
  recordedTraces() {
@@ -511,4 +589,19 @@ export class McpContext {
511
589
  });
512
590
  await this.#networkCollector.init(await this.browser.pages());
513
591
  }
592
+ async installExtension(extensionPath) {
593
+ const id = await this.browser.installExtension(extensionPath);
594
+ await this.#extensionRegistry.registerExtension(id, extensionPath);
595
+ return id;
596
+ }
597
+ async uninstallExtension(id) {
598
+ await this.browser.uninstallExtension(id);
599
+ this.#extensionRegistry.remove(id);
600
+ }
601
+ listExtensions() {
602
+ return this.#extensionRegistry.list();
603
+ }
604
+ getExtension(id) {
605
+ return this.#extensionRegistry.getById(id);
606
+ }
514
607
  }