chrome-devtools-mcp 0.10.2 → 0.12.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 (121) hide show
  1. package/README.md +73 -10
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/common.js +1 -3
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/AidaClient.js +1 -1
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHost.js +31 -449
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +430 -0
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +10 -22
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +2 -5
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/collect-ui-strings.js +1 -1
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/generate-locales-js.js +1 -1
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/HostRuntime.js +19 -0
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +1 -1
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/api/HostRuntime.js +4 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/api/api.js +5 -0
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/{common/Worker.js → platform/browser/HostRuntime.js} +18 -7
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/browser/browser.js +5 -0
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/node/HostRuntime.js +72 -0
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/node/node.js +5 -0
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/platform.js +2 -2
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/root/DevToolsContext.js +4 -0
  20. package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +7 -0
  21. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/AnimationModel.js +1 -1
  22. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +2 -2
  23. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +17 -5
  24. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +3 -3
  25. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSProperty.js +1 -1
  26. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +10 -10
  27. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ConsoleModel.js +1 -1
  28. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Cookie.js +1 -1
  29. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +5 -2
  30. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +1 -1
  31. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +46 -34
  32. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +3 -0
  33. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/PageResourceLoader.js +43 -33
  34. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/PreloadingModel.js +1 -1
  35. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RemoteObject.js +1 -1
  36. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -1
  37. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Script.js +26 -1
  38. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +4 -0
  39. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapCache.js +16 -0
  40. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +11 -5
  41. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +129 -20
  42. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Target.js +4 -13
  43. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TargetManager.js +35 -4
  44. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/sdk-meta.js +72 -0
  45. package/build/node_modules/chrome-devtools-frontend/front_end/foundation/Universe.js +5 -1
  46. package/build/node_modules/chrome-devtools-frontend/front_end/generated/Deprecation.js +48 -4
  47. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +45 -42
  48. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +56 -77
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +21 -6
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +9 -3
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +4 -3
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +34 -0
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +15 -0
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +59 -0
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +38 -0
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +5 -4
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/cpu_profile/CPUProfileDataModel.js +9 -7
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +5 -3
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +2 -2
  60. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/AttributionReportingIssue.js +5 -6
  61. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/BounceTrackingIssue.js +3 -11
  62. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/ClientHintIssue.js +4 -9
  63. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/ContentSecurityPolicyIssue.js +4 -9
  64. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/ContrastCheckTrigger.js +1 -1
  65. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/CookieDeprecationMetadataIssue.js +5 -11
  66. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/CookieIssue.js +26 -25
  67. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/CorsIssue.js +7 -14
  68. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/CrossOriginEmbedderPolicyIssue.js +4 -6
  69. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/DeprecationIssue.js +6 -11
  70. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/ElementAccessibilityIssue.js +6 -11
  71. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/FederatedAuthRequestIssue.js +3 -8
  72. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/FederatedAuthUserInfoRequestIssue.js +3 -8
  73. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/GenericIssue.js +36 -21
  74. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/HeavyAdIssue.js +3 -8
  75. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/Issue.js +6 -1
  76. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/IssueAggregator.js +6 -1
  77. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/LowTextContrastIssue.js +2 -7
  78. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/MixedContentIssue.js +6 -10
  79. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/PartitioningBlobURLIssue.js +3 -8
  80. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/PropertyRuleIssue.js +5 -10
  81. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/QuirksModeIssue.js +2 -7
  82. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/SRIMessageSignatureIssue.js +6 -10
  83. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/SharedArrayBufferIssue.js +3 -8
  84. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/SharedDictionaryIssue.js +5 -10
  85. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/StylesheetLoadingIssue.js +7 -11
  86. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/UnencodedDigestIssue.js +1 -6
  87. package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/genericNavigationEntryMarkedSkippable.md +7 -0
  88. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/FunctionCodeResolver.js +192 -0
  89. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +2 -1
  90. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/SamplesHandler.js +3 -0
  91. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +10 -0
  92. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +23 -0
  93. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +1 -1
  94. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +38 -0
  95. package/build/node_modules/chrome-devtools-frontend/mcp/HostBindings.js +222 -0
  96. package/build/node_modules/chrome-devtools-frontend/mcp/mcp.js +15 -1
  97. package/build/src/DevToolsConnectionAdapter.js +56 -19
  98. package/build/src/DevtoolsUtils.js +143 -1
  99. package/build/src/McpContext.js +33 -11
  100. package/build/src/McpResponse.js +18 -26
  101. package/build/src/PageCollector.js +4 -11
  102. package/build/src/browser.js +52 -2
  103. package/build/src/cli.js +31 -3
  104. package/build/src/formatters/consoleFormatter.js +81 -3
  105. package/build/src/formatters/snapshotFormatter.js +18 -4
  106. package/build/src/issue-descriptions.js +4 -0
  107. package/build/src/main.js +27 -50
  108. package/build/src/third_party/THIRD_PARTY_NOTICES +27 -27
  109. package/build/src/third_party/index.js +21898 -15109
  110. package/build/src/tools/console.js +0 -4
  111. package/build/src/tools/emulation.js +29 -6
  112. package/build/src/tools/screenshot.js +4 -2
  113. package/build/src/tools/snapshot.js +1 -1
  114. package/build/src/tools/tools.js +29 -0
  115. package/build/src/utils/keyboard.js +5 -0
  116. package/package.json +7 -7
  117. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Linkifier.js +0 -34
  118. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/DOMUtilities.js +0 -122
  119. package/build/src/features.js +0 -14
  120. /package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/{genericFormAriaLabelledByToNonExistingId.md → genericFormAriaLabelledByToNonExistingIdError.md} +0 -0
  121. /package/build/node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/{genericFormLabelHasNeitherForNorNestedInput.md → genericFormLabelHasNeitherForNorNestedInputError.md} +0 -0
@@ -3,7 +3,11 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { Common, I18n, } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
6
+ import { DebuggerModel, Foundation, TargetManager, MarkdownIssueDescription, Marked, ProtocolClient, Common, I18n, } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
7
+ import { PuppeteerDevToolsConnection } from './DevToolsConnectionAdapter.js';
8
+ import { ISSUE_UTILS } from './issue-descriptions.js';
9
+ import { logger } from './logger.js';
10
+ import { Mutex } from './Mutex.js';
7
11
  export function extractUrlLikeFromDevToolsTitle(title) {
8
12
  const match = title.match(new RegExp(`DevTools - (.*)`));
9
13
  return match?.[1] ?? undefined;
@@ -20,6 +24,7 @@ export function urlsEqual(url1, url2) {
20
24
  * 1. We do not care about the protocol.
21
25
  * 2. We do not care about trailing slashes.
22
26
  * 3. We do not care about "www".
27
+ * 4. We ignore the hash parts.
23
28
  *
24
29
  * For example, if the user types "record a trace on foo.com", we would want to
25
30
  * match a tab in the connected Chrome instance that is showing "www.foo.com/"
@@ -37,6 +42,12 @@ function normalizeUrl(url) {
37
42
  if (result.startsWith('www.')) {
38
43
  result = result.slice(4);
39
44
  }
45
+ // We use target URLs to locate DevTools but those often do
46
+ // no include hash.
47
+ const hashIdx = result.lastIndexOf('#');
48
+ if (hashIdx !== -1) {
49
+ result = result.slice(0, hashIdx);
50
+ }
40
51
  // Remove trailing slash
41
52
  if (result.endsWith('/')) {
42
53
  result = result.slice(0, -1);
@@ -53,6 +64,46 @@ export class FakeIssuesManager extends Common.ObjectWrapper
53
64
  return [];
54
65
  }
55
66
  }
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 = MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription.substitutions);
86
+ const markdownAst = Marked.Marked.lexer(processedMarkdown);
87
+ title = MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
88
+ }
89
+ catch {
90
+ logger('error parsing markdown for issue ' + issue.code());
91
+ return null;
92
+ }
93
+ if (!title) {
94
+ logger('cannot read issue title from ' + filename);
95
+ return null;
96
+ }
97
+ return {
98
+ type: 'issue',
99
+ item: issue,
100
+ message: title,
101
+ count,
102
+ description: processedMarkdown,
103
+ };
104
+ }
105
+ // DevTools CDP errors can get noisy.
106
+ ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
56
107
  I18n.DevToolsLocale.DevToolsLocale.instance({
57
108
  create: true,
58
109
  data: {
@@ -62,3 +113,94 @@ I18n.DevToolsLocale.DevToolsLocale.instance({
62
113
  },
63
114
  });
64
115
  I18n.i18n.registerLocaleDataForTest('en-US', {});
116
+ export class UniverseManager {
117
+ #browser;
118
+ #createUniverseFor;
119
+ #universes = new WeakMap();
120
+ /** Guard access to #universes so we don't create unnecessary universes */
121
+ #mutex = new Mutex();
122
+ constructor(browser, factory = DEFAULT_FACTORY) {
123
+ this.#browser = browser;
124
+ this.#createUniverseFor = factory;
125
+ }
126
+ async init(pages) {
127
+ try {
128
+ await this.#mutex.acquire();
129
+ const promises = [];
130
+ for (const page of pages) {
131
+ promises.push(this.#createUniverseFor(page).then(targetUniverse => this.#universes.set(page, targetUniverse)));
132
+ }
133
+ this.#browser.on('targetcreated', this.#onTargetCreated);
134
+ this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
135
+ await Promise.all(promises);
136
+ }
137
+ finally {
138
+ this.#mutex.release();
139
+ }
140
+ }
141
+ get(page) {
142
+ return this.#universes.get(page) ?? null;
143
+ }
144
+ dispose() {
145
+ this.#browser.off('targetcreated', this.#onTargetCreated);
146
+ this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
147
+ }
148
+ #onTargetCreated = async (target) => {
149
+ const page = await target.page();
150
+ try {
151
+ await this.#mutex.acquire();
152
+ if (!page || this.#universes.has(page)) {
153
+ return;
154
+ }
155
+ this.#universes.set(page, await this.#createUniverseFor(page));
156
+ }
157
+ finally {
158
+ this.#mutex.release();
159
+ }
160
+ };
161
+ #onTargetDestroyed = async (target) => {
162
+ const page = await target.page();
163
+ try {
164
+ await this.#mutex.acquire();
165
+ if (!page || !this.#universes.has(page)) {
166
+ return;
167
+ }
168
+ this.#universes.delete(page);
169
+ }
170
+ finally {
171
+ this.#mutex.release();
172
+ }
173
+ };
174
+ }
175
+ const DEFAULT_FACTORY = async (page) => {
176
+ const settingStorage = new Common.Settings.SettingsStorage({});
177
+ const universe = new Foundation.Universe.Universe({
178
+ settingsCreationOptions: {
179
+ syncedStorage: settingStorage,
180
+ globalStorage: settingStorage,
181
+ localStorage: settingStorage,
182
+ settingRegistrations: Common.SettingRegistration.getRegisteredSettings(),
183
+ },
184
+ overrideAutoStartModels: new Set([DebuggerModel]),
185
+ });
186
+ const session = await page.createCDPSession();
187
+ const connection = new PuppeteerDevToolsConnection(session);
188
+ const targetManager = universe.context.get(TargetManager);
189
+ targetManager.observeModels(DebuggerModel, SKIP_ALL_PAUSES);
190
+ const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any
191
+ /* parentTarget */ null, session.id(), undefined, connection);
192
+ return { target, universe };
193
+ };
194
+ // We don't want to pause any DevTools universe session ever on the MCP side.
195
+ //
196
+ // Note that calling `setSkipAllPauses` only affects the session on which it was
197
+ // sent. This means DevTools can still pause, step and do whatever. We just won't
198
+ // see the `Debugger.paused`/`Debugger.resumed` events on the MCP side.
199
+ const SKIP_ALL_PAUSES = {
200
+ modelAdded(model) {
201
+ void model.agent.invoke_setSkipAllPauses({ skip: true });
202
+ },
203
+ modelRemoved() {
204
+ // Do nothing.
205
+ },
206
+ };
@@ -54,6 +54,7 @@ export class McpContext {
54
54
  #isRunningTrace = false;
55
55
  #networkConditionsMap = new WeakMap();
56
56
  #cpuThrottlingRateMap = new WeakMap();
57
+ #geolocationMap = new WeakMap();
57
58
  #dialog;
58
59
  #nextSnapshotId = 1;
59
60
  #traceResults = [];
@@ -64,7 +65,7 @@ export class McpContext {
64
65
  this.logger = logger;
65
66
  this.#locatorClass = locatorClass;
66
67
  this.#options = options;
67
- this.#networkCollector = new NetworkCollector(this.browser, undefined, this.#options.experimentalIncludeAllPages);
68
+ this.#networkCollector = new NetworkCollector(this.browser);
68
69
  this.#consoleCollector = new ConsoleCollector(this.browser, collect => {
69
70
  return {
70
71
  console: event => {
@@ -84,12 +85,12 @@ export class McpContext {
84
85
  collect(event);
85
86
  },
86
87
  };
87
- }, this.#options.experimentalIncludeAllPages);
88
+ });
88
89
  }
89
90
  async #init() {
90
- await this.createPagesSnapshot();
91
- await this.#networkCollector.init();
92
- await this.#consoleCollector.init();
91
+ const pages = await this.createPagesSnapshot();
92
+ await this.#networkCollector.init(pages);
93
+ await this.#consoleCollector.init(pages);
93
94
  }
94
95
  dispose() {
95
96
  this.#networkCollector.dispose();
@@ -123,8 +124,12 @@ export class McpContext {
123
124
  this.logger('no cdpBackendNodeId');
124
125
  return;
125
126
  }
127
+ if (this.#textSnapshot === null) {
128
+ this.logger('no text snapshot');
129
+ return;
130
+ }
126
131
  // TODO: index by backendNodeId instead.
127
- const queue = [this.#textSnapshot?.root];
132
+ const queue = [this.#textSnapshot.root];
128
133
  while (queue.length) {
129
134
  const current = queue.pop();
130
135
  if (current.backendNodeId === cdpBackendNodeId) {
@@ -191,6 +196,19 @@ export class McpContext {
191
196
  const page = this.getSelectedPage();
192
197
  return this.#cpuThrottlingRateMap.get(page) ?? 1;
193
198
  }
199
+ setGeolocation(geolocation) {
200
+ const page = this.getSelectedPage();
201
+ if (geolocation === null) {
202
+ this.#geolocationMap.delete(page);
203
+ }
204
+ else {
205
+ this.#geolocationMap.set(page, geolocation);
206
+ }
207
+ }
208
+ getGeolocation() {
209
+ const page = this.getSelectedPage();
210
+ return this.#geolocationMap.get(page) ?? null;
211
+ }
194
212
  setIsRunningPerformanceTrace(x) {
195
213
  this.#isRunningTrace = x;
196
214
  }
@@ -284,7 +302,8 @@ export class McpContext {
284
302
  return (this.#options.experimentalDevToolsDebugging ||
285
303
  !page.url().startsWith('devtools://'));
286
304
  });
287
- if (!this.#selectedPage || this.#pages.indexOf(this.#selectedPage) === -1) {
305
+ if ((!this.#selectedPage || this.#pages.indexOf(this.#selectedPage) === -1) &&
306
+ this.#pages[0]) {
288
307
  this.selectPage(this.#pages[0]);
289
308
  }
290
309
  await this.detectOpenDevToolsWindows();
@@ -395,9 +414,12 @@ export class McpContext {
395
414
  root: rootNodeWithId,
396
415
  snapshotId: String(snapshotId),
397
416
  idToNode,
417
+ hasSelectedElement: false,
418
+ verbose,
398
419
  };
399
420
  const data = devtoolsData ?? (await this.getDevToolsData());
400
421
  if (data?.cdpBackendNodeId) {
422
+ this.#textSnapshot.hasSelectedElement = true;
401
423
  this.#textSnapshot.selectedElementUid = this.resolveCdpElementId(data?.cdpBackendNodeId);
402
424
  }
403
425
  }
@@ -446,15 +468,15 @@ export class McpContext {
446
468
  getNetworkRequestStableId(request) {
447
469
  return this.#networkCollector.getIdForResource(request);
448
470
  }
449
- waitForTextOnPage({ text, timeout, }) {
471
+ waitForTextOnPage(text, timeout) {
450
472
  const page = this.getSelectedPage();
451
473
  const frames = page.frames();
452
- const locator = this.#locatorClass.race(frames.flatMap(frame => [
474
+ let locator = this.#locatorClass.race(frames.flatMap(frame => [
453
475
  frame.locator(`aria/${text}`),
454
476
  frame.locator(`text/${text}`),
455
477
  ]));
456
478
  if (timeout) {
457
- locator.setTimeout(timeout);
479
+ locator = locator.setTimeout(timeout);
458
480
  }
459
481
  return locator.wait();
460
482
  }
@@ -472,6 +494,6 @@ export class McpContext {
472
494
  },
473
495
  };
474
496
  });
475
- await this.#networkCollector.init();
497
+ await this.#networkCollector.init(await this.browser.pages());
476
498
  }
477
499
  }
@@ -3,12 +3,11 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { AggregatedIssue, Marked, findTitleFromMarkdownAst, } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
6
+ import { AggregatedIssue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
7
+ import { mapIssueToMessageObject } from './DevtoolsUtils.js';
7
8
  import { formatConsoleEventShort, formatConsoleEventVerbose, } from './formatters/consoleFormatter.js';
8
9
  import { getFormattedHeaderValue, getFormattedResponseBody, getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
9
10
  import { formatSnapshotNode } from './formatters/snapshotFormatter.js';
10
- import { getIssueDescription } from './issue-descriptions.js';
11
- import { logger } from './logger.js';
12
11
  import { handleDialog } from './tools/pages.js';
13
12
  import { paginate } from './utils/pagination.js';
14
13
  export class McpResponse {
@@ -156,6 +155,15 @@ export class McpResponse {
156
155
  })),
157
156
  };
158
157
  }
158
+ else if (message instanceof AggregatedIssue) {
159
+ const mappedIssueMessage = mapIssueToMessageObject(message);
160
+ if (!mappedIssueMessage)
161
+ throw new Error("Can't provide detals for the msgid " + consoleMessageStableId);
162
+ consoleData = {
163
+ consoleMessageStableId,
164
+ ...mappedIssueMessage,
165
+ };
166
+ }
159
167
  else {
160
168
  consoleData = {
161
169
  consoleMessageStableId,
@@ -199,28 +207,12 @@ export class McpResponse {
199
207
  };
200
208
  }
201
209
  if (item instanceof AggregatedIssue) {
202
- const count = item.getAggregatedIssuesCount();
203
- const filename = item.getDescription()?.file;
204
- const rawMarkdown = filename
205
- ? getIssueDescription(filename)
206
- : null;
207
- if (!rawMarkdown) {
208
- logger(`no markdown ${filename} found for issue:` + item.code);
210
+ const mappedIssueMessage = mapIssueToMessageObject(item);
211
+ if (!mappedIssueMessage)
209
212
  return null;
210
- }
211
- const markdownAst = Marked.Marked.lexer(rawMarkdown);
212
- const title = findTitleFromMarkdownAst(markdownAst);
213
- if (!title) {
214
- logger('cannot read issue title from ' + filename);
215
- return null;
216
- }
217
213
  return {
218
214
  consoleMessageStableId,
219
- type: 'issue',
220
- item,
221
- message: title,
222
- count,
223
- args: [],
215
+ ...mappedIssueMessage,
224
216
  };
225
217
  }
226
218
  return {
@@ -273,11 +265,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
273
265
  response.push(...parts);
274
266
  }
275
267
  if (data.formattedSnapshot) {
276
- response.push('## Page content');
268
+ response.push('## Latest page snapshot');
277
269
  response.push(data.formattedSnapshot);
278
270
  }
279
271
  response.push(...this.#formatNetworkRequestData(context, data.bodies));
280
- response.push(...this.#formatConsoleData(data.consoleData));
272
+ response.push(...this.#formatConsoleData(context, data.consoleData));
281
273
  if (this.#networkRequestsOptions?.include) {
282
274
  let requests = context.getNetworkRequests(this.#networkRequestsOptions?.includePreservedRequests);
283
275
  // Apply resource type filtering if specified
@@ -346,12 +338,12 @@ Call ${handleDialog.name} to handle it before continuing.`);
346
338
  items: paginationResult.items,
347
339
  };
348
340
  }
349
- #formatConsoleData(data) {
341
+ #formatConsoleData(context, data) {
350
342
  const response = [];
351
343
  if (!data) {
352
344
  return response;
353
345
  }
354
- response.push(formatConsoleEventVerbose(data));
346
+ response.push(formatConsoleEventVerbose(data, context));
355
347
  return response;
356
348
  }
357
349
  #formatNetworkRequestData(context, data) {
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import { createIssuesFromProtocolIssue, IssueAggregator, } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
7
7
  import { FakeIssuesManager } from './DevtoolsUtils.js';
8
- import { features } from './features.js';
9
8
  import { logger } from './logger.js';
10
9
  function createIdGenerator() {
11
10
  let i = 1;
@@ -22,20 +21,17 @@ export class PageCollector {
22
21
  #listenersInitializer;
23
22
  #listeners = new WeakMap();
24
23
  #maxNavigationSaved = 3;
25
- #includeAllPages;
26
24
  /**
27
25
  * This maps a Page to a list of navigations with a sub-list
28
26
  * of all collected resources.
29
27
  * The newer navigations come first.
30
28
  */
31
29
  storage = new WeakMap();
32
- constructor(browser, listeners, includeAllPages) {
30
+ constructor(browser, listeners) {
33
31
  this.#browser = browser;
34
32
  this.#listenersInitializer = listeners;
35
- this.#includeAllPages = includeAllPages;
36
33
  }
37
- async init() {
38
- const pages = await this.#browser.pages(this.#includeAllPages);
34
+ async init(pages) {
39
35
  for (const page of pages) {
40
36
  this.addPage(page);
41
37
  }
@@ -154,9 +150,6 @@ export class ConsoleCollector extends PageCollector {
154
150
  #subscribedPages = new WeakMap();
155
151
  addPage(page) {
156
152
  super.addPage(page);
157
- if (!features.issues) {
158
- return;
159
- }
160
153
  if (!this.#subscribedPages.has(page)) {
161
154
  const subscriber = new PageIssueSubscriber(page);
162
155
  this.#subscribedPages.set(page, subscriber);
@@ -262,8 +255,8 @@ export class NetworkCollector extends PageCollector {
262
255
  collect(req);
263
256
  },
264
257
  };
265
- }, includeAllPages) {
266
- super(browser, listeners, includeAllPages);
258
+ }) {
259
+ super(browser, listeners);
267
260
  }
268
261
  splitAfterNavigation(page) {
269
262
  const navigations = this.storage.get(page) ?? [];
@@ -19,6 +19,10 @@ function makeTargetFilter() {
19
19
  if (target.url() === 'chrome://newtab/') {
20
20
  return true;
21
21
  }
22
+ // Could be the only page opened in the browser.
23
+ if (target.url().startsWith('chrome://inspect')) {
24
+ return true;
25
+ }
22
26
  for (const prefix of ignoredPrefixes) {
23
27
  if (target.url().startsWith(prefix)) {
24
28
  return false;
@@ -28,6 +32,7 @@ function makeTargetFilter() {
28
32
  };
29
33
  }
30
34
  export async function ensureBrowserConnected(options) {
35
+ const { channel } = options;
31
36
  if (browser?.connected) {
32
37
  return browser;
33
38
  }
@@ -45,11 +50,56 @@ export async function ensureBrowserConnected(options) {
45
50
  else if (options.browserURL) {
46
51
  connectOptions.browserURL = options.browserURL;
47
52
  }
53
+ else if (channel || options.userDataDir) {
54
+ const userDataDir = options.userDataDir;
55
+ if (userDataDir) {
56
+ // TODO: re-expose this logic via Puppeteer.
57
+ const portPath = path.join(userDataDir, 'DevToolsActivePort');
58
+ try {
59
+ const fileContent = await fs.promises.readFile(portPath, 'utf8');
60
+ const [rawPort, rawPath] = fileContent
61
+ .split('\n')
62
+ .map(line => {
63
+ return line.trim();
64
+ })
65
+ .filter(line => {
66
+ return !!line;
67
+ });
68
+ if (!rawPort || !rawPath) {
69
+ throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
70
+ }
71
+ const port = parseInt(rawPort, 10);
72
+ if (isNaN(port) || port <= 0 || port > 65535) {
73
+ throw new Error(`Invalid port '${rawPort}' found`);
74
+ }
75
+ const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
76
+ connectOptions.browserWSEndpoint = browserWSEndpoint;
77
+ }
78
+ catch (error) {
79
+ throw new Error(`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled.`, {
80
+ cause: error,
81
+ });
82
+ }
83
+ }
84
+ else {
85
+ if (!channel) {
86
+ throw new Error('Channel must be provided if userDataDir is missing');
87
+ }
88
+ connectOptions.channel = (channel === 'stable' ? 'chrome' : `chrome-${channel}`);
89
+ }
90
+ }
48
91
  else {
49
- throw new Error('Either browserURL or wsEndpoint must be provided');
92
+ throw new Error('Either browserURL, wsEndpoint, channel or userDataDir must be provided');
50
93
  }
51
94
  logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
52
- browser = await puppeteer.connect(connectOptions);
95
+ try {
96
+ browser = await puppeteer.connect(connectOptions);
97
+ }
98
+ catch (err) {
99
+ throw new Error('Could not connect to Chrome. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.', {
100
+ cause: err,
101
+ });
102
+ }
53
103
  logger('Connected Puppeteer');
54
104
  return browser;
55
105
  }
package/build/src/cli.js CHANGED
@@ -5,9 +5,21 @@
5
5
  */
6
6
  import { yargs, hideBin } from './third_party/index.js';
7
7
  export const cliOptions = {
8
+ autoConnect: {
9
+ type: 'boolean',
10
+ description: '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.',
11
+ conflicts: ['isolated', 'executablePath'],
12
+ default: false,
13
+ coerce: (value) => {
14
+ if (!value) {
15
+ return;
16
+ }
17
+ return value;
18
+ },
19
+ },
8
20
  browserUrl: {
9
21
  type: 'string',
10
- description: 'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.',
22
+ description: '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.',
11
23
  alias: 'u',
12
24
  conflicts: 'wsEndpoint',
13
25
  coerce: (url) => {
@@ -80,8 +92,12 @@ export const cliOptions = {
80
92
  },
81
93
  isolated: {
82
94
  type: 'boolean',
83
- description: 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.',
84
- default: false,
95
+ description: 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false.',
96
+ },
97
+ userDataDir: {
98
+ type: 'string',
99
+ description: 'Path to the user data directory for Chrome. Default is $HOME/.cache/chrome-devtools-mcp/chrome-profile$CHANNEL_SUFFIX_IF_NON_STABLE',
100
+ conflicts: ['browserUrl', 'wsEndpoint', 'isolated'],
85
101
  },
86
102
  channel: {
87
103
  type: 'string',
@@ -196,6 +212,18 @@ export function parseArguments(version, argv = process.argv) {
196
212
  'Disable tools in the performance category',
197
213
  ],
198
214
  ['$0 --no-category-network', 'Disable tools in the network category'],
215
+ [
216
+ '$0 --user-data-dir=/tmp/user-data-dir',
217
+ 'Use a custom user data directory',
218
+ ],
219
+ [
220
+ '$0 --auto-connect',
221
+ 'Connect to a stable Chrome instance (Chrome 145+) running instead of launching a new instance',
222
+ ],
223
+ [
224
+ '$0 --auto-connect --channel=canary',
225
+ 'Connect to a canary Chrome instance (Chrome 145+) running instead of launching a new instance',
226
+ ],
199
227
  ]);
200
228
  return yargsInstance
201
229
  .wrap(Math.min(120, yargsInstance.terminalWidth()))
@@ -19,11 +19,12 @@ function getArgs(msg) {
19
19
  return args;
20
20
  }
21
21
  // The verbose format for a console message, including all details.
22
- export function formatConsoleEventVerbose(msg) {
22
+ export function formatConsoleEventVerbose(msg, context) {
23
+ const aggregatedIssue = msg.item;
23
24
  const result = [
24
25
  `ID: ${msg.consoleMessageStableId}`,
25
- `Message: ${msg.type}> ${msg.message}`,
26
- formatArgs(msg),
26
+ `Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description, context) : msg.message}`,
27
+ aggregatedIssue ? undefined : formatArgs(msg),
27
28
  ].filter(line => !!line);
28
29
  return result.join('\n');
29
30
  }
@@ -41,3 +42,80 @@ function formatArgs(consoleData) {
41
42
  }
42
43
  return result.join('\n');
43
44
  }
45
+ export function formatIssue(issue, description, context) {
46
+ const result = [];
47
+ let processedMarkdown = description?.trim();
48
+ // Remove heading in order not to conflict with the whole console message response markdown
49
+ if (processedMarkdown?.startsWith('# ')) {
50
+ processedMarkdown = processedMarkdown.substring(2).trimStart();
51
+ }
52
+ if (processedMarkdown)
53
+ result.push(processedMarkdown);
54
+ const links = issue.getDescription()?.links;
55
+ if (links && links.length > 0) {
56
+ result.push('Learn more:');
57
+ for (const link of links) {
58
+ result.push(`[${link.linkTitle}](${link.link})`);
59
+ }
60
+ }
61
+ const issues = issue.getAllIssues();
62
+ const affectedResources = [];
63
+ for (const singleIssue of issues) {
64
+ const details = singleIssue.details();
65
+ if (!details)
66
+ continue;
67
+ // We send the remaining details as untyped JSON because the DevTools
68
+ // frontend code is currently not re-usable.
69
+ // eslint-disable-next-line
70
+ const data = structuredClone(details);
71
+ let uid;
72
+ let request;
73
+ if ('violatingNodeId' in details && details.violatingNodeId && context) {
74
+ uid = context.resolveCdpElementId(details.violatingNodeId);
75
+ delete data.violatingNodeId;
76
+ }
77
+ if ('nodeId' in details && details.nodeId && context) {
78
+ uid = context.resolveCdpElementId(details.nodeId);
79
+ delete data.nodeId;
80
+ }
81
+ if ('documentNodeId' in details && details.documentNodeId && context) {
82
+ uid = context.resolveCdpElementId(details.documentNodeId);
83
+ delete data.documentNodeId;
84
+ }
85
+ if ('request' in details && details.request) {
86
+ request = details.request.url;
87
+ if (details.request.requestId && context) {
88
+ const resolvedId = context.resolveCdpRequestId(details.request.requestId);
89
+ if (resolvedId) {
90
+ request = resolvedId;
91
+ delete data.request.requestId;
92
+ }
93
+ }
94
+ }
95
+ // These fields has no use for the MCP client (redundant or irrelevant).
96
+ delete data.errorType;
97
+ delete data.frameId;
98
+ affectedResources.push({
99
+ uid,
100
+ data: data,
101
+ request,
102
+ });
103
+ }
104
+ if (affectedResources.length) {
105
+ result.push('### Affected resources');
106
+ }
107
+ result.push(...affectedResources.map(item => {
108
+ const details = [];
109
+ if (item.uid)
110
+ details.push(`uid=${item.uid}`);
111
+ if (item.request) {
112
+ details.push((typeof item.request === 'number' ? `reqid=` : 'url=') + item.request);
113
+ }
114
+ if (item.data)
115
+ details.push(`data=${JSON.stringify(item.data)}`);
116
+ return details.join(' ');
117
+ }));
118
+ if (result.length === 0)
119
+ return 'No affected resources found';
120
+ return result.join('\n');
121
+ }