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.
Files changed (37) hide show
  1. package/README.md +9 -2
  2. package/build/src/DevtoolsUtils.js +59 -42
  3. package/build/src/McpContext.js +92 -13
  4. package/build/src/McpResponse.js +207 -132
  5. package/build/src/browser.js +1 -0
  6. package/build/src/cli.js +9 -3
  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 +13 -3
  13. package/build/src/telemetry/clearcut-logger.js +83 -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 +48 -0
  19. package/build/src/telemetry/watchdog/main.js +98 -0
  20. package/build/src/telemetry/watchdog-client.js +51 -0
  21. package/build/src/third_party/THIRD_PARTY_NOTICES +0 -1417
  22. package/build/src/third_party/devtools-formatter-worker.js +15451 -0
  23. package/build/src/third_party/index.js +1252 -205
  24. package/build/src/tools/categories.js +2 -0
  25. package/build/src/tools/emulation.js +62 -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 +75 -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
@@ -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<a/> to install a custom MCP server. Add the following config to the MCP servers config:
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 145+) running in the user data directory identified by the channel param. Requires remote debugging being enabled in Chrome here: chrome://inspect/#remote-debugging.
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
+ }
@@ -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 handle = await node.elementHandle();
299
- if (!handle) {
300
- throw new Error('No such element found in the snapshot');
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: `${snapshotId}_${idCounter++}`,
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
  }