chrome-devtools-mcp 0.16.0 → 0.17.1

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 (29) hide show
  1. package/README.md +56 -9
  2. package/build/src/DevtoolsUtils.js +113 -0
  3. package/build/src/McpContext.js +3 -0
  4. package/build/src/McpResponse.js +1 -8
  5. package/build/src/PageCollector.js +4 -9
  6. package/build/src/cli.js +9 -0
  7. package/build/src/formatters/ConsoleFormatter.js +144 -72
  8. package/build/src/main.js +8 -4
  9. package/build/src/telemetry/{clearcut-logger.js → ClearcutLogger.js} +1 -1
  10. package/build/src/telemetry/watchdog/main.js +1 -1
  11. package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
  12. package/build/src/third_party/bundled-packages.json +3 -3
  13. package/build/src/third_party/devtools-formatter-worker.js +0 -2
  14. package/build/src/third_party/index.js +1424 -1311
  15. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowlistItemType.md +12 -0
  16. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidHeader.md +12 -0
  17. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidUrlPattern.md +8 -0
  18. package/build/src/third_party/issue-descriptions/connectionAllowlistItemNotInnerList.md +12 -0
  19. package/build/src/third_party/issue-descriptions/connectionAllowlistMoreThanOneList.md +7 -0
  20. package/build/src/third_party/issue-descriptions/connectionAllowlistReportingEndpointNotToken.md +10 -0
  21. package/build/src/tools/performance.js +31 -1
  22. package/package.json +5 -4
  23. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataHttpNotFound.md +0 -1
  24. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataInvalidResponse.md +0 -1
  25. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataNoResponse.md +0 -1
  26. /package/build/src/telemetry/{watchdog-client.js → WatchdogClient.js} +0 -0
  27. /package/build/src/telemetry/{flag-utils.js → flagUtils.js} +0 -0
  28. /package/build/src/telemetry/{metric-utils.js → metricUtils.js} +0 -0
  29. /package/build/src/telemetry/watchdog/{clearcut-sender.js → ClearcutSender.js} +0 -0
package/README.md CHANGED
@@ -27,6 +27,12 @@ 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
+ Performance tools may send trace URLs to the Google CrUX API to fetch real-user
31
+ experience data. This helps provide a holistic performance picture by
32
+ presenting field data alongside lab data. This data is collected by the [Chrome
33
+ User Experience Report (CrUX)](https://developer.chrome.com/docs/crux). To disable
34
+ this, run with the `--no-performance-crux` flag.
35
+
30
36
  ## **Usage statistics**
31
37
 
32
38
  Google collects usage statistics (such as tool invocation success rates, latency, and environment information) to improve the reliability and performance of Chrome DevTools MCP.
@@ -107,12 +113,31 @@ Chrome DevTools MCP will not start the browser instance automatically using this
107
113
 
108
114
  <details>
109
115
  <summary>Claude Code</summary>
110
- Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://code.claude.com/docs/en/mcp">guide</a>):
116
+
117
+ **Install via CLI (MCP only)**
118
+
119
+ Use the Claude Code CLI to add the Chrome DevTools MCP server (<a href="https://code.claude.com/docs/en/mcp">guide</a>):
111
120
 
112
121
  ```bash
113
122
  claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest
114
123
  ```
115
124
 
125
+ **Install as a Plugin (MCP + Skills)**
126
+
127
+ To install Chrome DevTools MCP with skills, add the marketplace registry in Claude Code:
128
+
129
+ ```sh
130
+ /plugin marketplace add ChromeDevTools/chrome-devtools-mcp
131
+ ```
132
+
133
+ Then, install the plugin:
134
+
135
+ ```sh
136
+ /plugin install chrome-devtools-mcp
137
+ ```
138
+
139
+ Restart Claude Code to have the MCP server and skills load (check with `/skills`).
140
+
116
141
  </details>
117
142
 
118
143
  <details>
@@ -260,6 +285,30 @@ Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Con
260
285
 
261
286
  </details>
262
287
 
288
+ <details>
289
+ <summary>Katalon Studio</summary>
290
+
291
+ The Chrome DevTools MCP server can be used with <a href="https://docs.katalon.com/katalon-studio/studioassist/mcp-servers/setting-up-chrome-devtools-mcp-server-for-studioassist">Katalon StudioAssist</a> via an MCP proxy.
292
+
293
+ **Step 1:** Install the MCP proxy by following the <a href="https://docs.katalon.com/katalon-studio/studioassist/mcp-servers/setting-up-mcp-proxy-for-stdio-mcp-servers">MCP proxy setup guide</a>.
294
+
295
+ **Step 2:** Start the Chrome DevTools MCP server with the proxy:
296
+
297
+ ```bash
298
+ mcp-proxy --transport streamablehttp --port 8080 -- npx -y chrome-devtools-mcp@latest
299
+ ```
300
+
301
+ **Note:** You may need to pick another port if 8080 is already in use.
302
+
303
+ **Step 3:** In Katalon Studio, add the server to StudioAssist with the following settings:
304
+
305
+ - **Connection URL:** `http://127.0.0.1:8080/mcp`
306
+ - **Transport type:** `HTTP`
307
+
308
+ Once connected, the Chrome DevTools MCP tools will be available in StudioAssist.
309
+
310
+ </details>
311
+
263
312
  <details>
264
313
  <summary>OpenCode</summary>
265
314
 
@@ -466,6 +515,11 @@ The Chrome DevTools MCP server supports the following configuration option:
466
515
  - **Type:** boolean
467
516
  - **Default:** `true`
468
517
 
518
+ - **`--performanceCrux`/ `--performance-crux`**
519
+ Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.
520
+ - **Type:** boolean
521
+ - **Default:** `true`
522
+
469
523
  - **`--usageStatistics`/ `--usage-statistics`**
470
524
  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. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
471
525
  - **Type:** boolean
@@ -658,11 +712,4 @@ Please consult [these instructions](./docs/debugging-android.md).
658
712
 
659
713
  ## Known limitations
660
714
 
661
- ### Operating system sandboxes
662
-
663
- Some MCP clients allow sandboxing the MCP server using macOS Seatbelt or Linux
664
- containers. If sandboxes are enabled, `chrome-devtools-mcp` is not able to start
665
- Chrome that requires permissions to create its own sandboxes. As a workaround,
666
- either disable sandboxing for `chrome-devtools-mcp` in your MCP client or use
667
- `--browser-url` to connect to a Chrome instance that you start manually outside
668
- of the MCP client sandbox.
715
+ See [Troubleshooting](./docs/troubleshooting.md).
@@ -169,6 +169,119 @@ const SKIP_ALL_PAUSES = {
169
169
  // Do nothing.
170
170
  },
171
171
  };
172
+ /**
173
+ * Constructed from Runtime.ExceptionDetails of an uncaught error.
174
+ *
175
+ * TODO: Also construct from a RemoteObject of subtype 'error'.
176
+ *
177
+ * Consists of the message, a fully resolved stack trace and a fully resolved 'cause' chain.
178
+ */
179
+ export class SymbolizedError {
180
+ message;
181
+ stackTrace;
182
+ cause;
183
+ constructor(message, stackTrace, cause) {
184
+ this.message = message;
185
+ this.stackTrace = stackTrace;
186
+ this.cause = cause;
187
+ }
188
+ static async fromDetails(opts) {
189
+ const message = SymbolizedError.#getMessage(opts.details);
190
+ if (!opts.includeStackAndCause || !opts.devTools) {
191
+ return new SymbolizedError(message, opts.resolvedStackTraceForTesting, opts.resolvedCauseForTesting);
192
+ }
193
+ let stackTrace;
194
+ if (opts.resolvedStackTraceForTesting) {
195
+ stackTrace = opts.resolvedStackTraceForTesting;
196
+ }
197
+ else if (opts.details.stackTrace) {
198
+ try {
199
+ stackTrace = await createStackTrace(opts.devTools, opts.details.stackTrace, opts.targetId);
200
+ }
201
+ catch {
202
+ // ignore
203
+ }
204
+ }
205
+ // TODO: Turn opts.details.exception into a JSHandle and retrieve the 'cause' property.
206
+ // If its an Error, recursively create a SymbolizedError.
207
+ let cause;
208
+ if (opts.resolvedCauseForTesting) {
209
+ cause = opts.resolvedCauseForTesting;
210
+ }
211
+ else if (opts.details.exception) {
212
+ try {
213
+ const causeRemoteObj = await SymbolizedError.#lookupCause(opts.devTools, opts.details.exception, opts.targetId);
214
+ if (causeRemoteObj) {
215
+ cause = await SymbolizedError.fromError({
216
+ devTools: opts.devTools,
217
+ error: causeRemoteObj,
218
+ targetId: opts.targetId,
219
+ });
220
+ }
221
+ }
222
+ catch {
223
+ // Ignore
224
+ }
225
+ }
226
+ return new SymbolizedError(message, stackTrace, cause);
227
+ }
228
+ static async fromError(opts) {
229
+ const details = await SymbolizedError.#getExceptionDetails(opts.devTools, opts.error, opts.targetId);
230
+ if (details) {
231
+ return SymbolizedError.fromDetails({
232
+ details,
233
+ devTools: opts.devTools,
234
+ targetId: opts.targetId,
235
+ includeStackAndCause: true,
236
+ });
237
+ }
238
+ return new SymbolizedError(SymbolizedError.#getMessageFromException(opts.error));
239
+ }
240
+ static #getMessage(details) {
241
+ // For Runtime.exceptionThrown with a present exception object, `details.text` will be "Uncaught" and
242
+ // we have to manually parse out the error text from the exception description.
243
+ // In the case of Runtime.getExceptionDetails, `details.text` has the Error.message.
244
+ if (details.text === 'Uncaught' && details.exception) {
245
+ return ('Uncaught ' +
246
+ SymbolizedError.#getMessageFromException(details.exception));
247
+ }
248
+ return details.text;
249
+ }
250
+ static #getMessageFromException(error) {
251
+ const messageWithRest = error.description?.split('\n at ', 2) ?? [];
252
+ return messageWithRest[0] ?? '';
253
+ }
254
+ static async #getExceptionDetails(devTools, error, targetId) {
255
+ if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) {
256
+ return null;
257
+ }
258
+ const targetManager = devTools.universe.context.get(DevTools.TargetManager);
259
+ const target = targetId
260
+ ? targetManager.targetById(targetId) || devTools.target
261
+ : devTools.target;
262
+ const model = target.model(DevTools.RuntimeModel);
263
+ return ((await model.getExceptionDetails(error.objectId)) ?? null);
264
+ }
265
+ static async #lookupCause(devTools, error, targetId) {
266
+ if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) {
267
+ return null;
268
+ }
269
+ const targetManager = devTools.universe.context.get(DevTools.TargetManager);
270
+ const target = targetId
271
+ ? targetManager.targetById(targetId) || devTools.target
272
+ : devTools.target;
273
+ const properties = await target.runtimeAgent().invoke_getProperties({
274
+ objectId: error.objectId,
275
+ });
276
+ if (properties.getError()) {
277
+ return null;
278
+ }
279
+ return properties.result.find(prop => prop.name === 'cause')?.value ?? null;
280
+ }
281
+ static createForTesting(message, stackTrace, cause) {
282
+ return new SymbolizedError(message, stackTrace, cause);
283
+ }
284
+ }
172
285
  export async function createStackTraceForConsoleMessage(devTools, consoleMessage) {
173
286
  const message = consoleMessage;
174
287
  const rawStackTrace = message._rawStackTrace();
@@ -262,6 +262,9 @@ export class McpContext {
262
262
  isRunningPerformanceTrace() {
263
263
  return this.#isRunningTrace;
264
264
  }
265
+ isCruxEnabled() {
266
+ return this.#options.performanceCrux;
267
+ }
265
268
  getDialog() {
266
269
  return this.#dialog;
267
270
  }
@@ -194,11 +194,6 @@ export class McpResponse {
194
194
  }
195
195
  detailedConsoleMessage = formatter;
196
196
  }
197
- else {
198
- detailedConsoleMessage = await ConsoleFormatter.from(message, {
199
- id: consoleMessageStableId,
200
- });
201
- }
202
197
  }
203
198
  let extensions;
204
199
  if (this.#listExtensions) {
@@ -239,9 +234,7 @@ export class McpResponse {
239
234
  }
240
235
  return formatter;
241
236
  }
242
- return await ConsoleFormatter.from(item, {
243
- id: consoleMessageStableId,
244
- });
237
+ return null;
245
238
  }))).filter(item => item !== null);
246
239
  }
247
240
  let networkRequests;
@@ -7,12 +7,10 @@ import { FakeIssuesManager } from './DevtoolsUtils.js';
7
7
  import { logger } from './logger.js';
8
8
  import { DevTools } from './third_party/index.js';
9
9
  export class UncaughtError {
10
- message;
11
- stackTrace;
10
+ details;
12
11
  targetId;
13
- constructor(message, stackTrace, targetId) {
14
- this.message = message;
15
- this.stackTrace = stackTrace;
12
+ constructor(details, targetId) {
13
+ this.details = details;
16
14
  this.targetId = targetId;
17
15
  }
18
16
  }
@@ -238,10 +236,7 @@ class PageEventSubscriber {
238
236
  this.#page.emit('issue', event.data);
239
237
  };
240
238
  #onExceptionThrown = (event) => {
241
- const { exception, text, stackTrace } = event.exceptionDetails;
242
- const messageWithRest = exception?.description?.split('\n at ', 2) ?? [];
243
- const message = text + ' ' + (messageWithRest[0] ?? '');
244
- this.#page.emit('uncaughtError', new UncaughtError(message, stackTrace, this.#targetId));
239
+ this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
245
240
  };
246
241
  // On navigation, we reset issue aggregation.
247
242
  #onFrameNavigated = (frame) => {
package/build/src/cli.js CHANGED
@@ -188,6 +188,11 @@ export const cliOptions = {
188
188
  hidden: true,
189
189
  describe: 'Set to false to exclude tools related to extensions.',
190
190
  },
191
+ performanceCrux: {
192
+ type: 'boolean',
193
+ default: true,
194
+ describe: 'Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.',
195
+ },
191
196
  usageStatistics: {
192
197
  type: 'boolean',
193
198
  default: true,
@@ -277,6 +282,10 @@ export function parseArguments(version, argv = process.argv) {
277
282
  '$0 --no-usage-statistics',
278
283
  'Do not send usage statistics https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics.',
279
284
  ],
285
+ [
286
+ '$0 --no-performance-crux',
287
+ 'Disable CrUX (field data) integration in performance tools.',
288
+ ],
280
289
  ]);
281
290
  return yargsInstance
282
291
  .wrap(Math.min(120, yargsInstance.terminalWidth()))
@@ -3,35 +3,80 @@
3
3
  * Copyright 2026 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { createStackTraceForConsoleMessage, createStackTrace, } from '../DevtoolsUtils.js';
6
+ var _a;
7
+ import { createStackTraceForConsoleMessage, SymbolizedError, } from '../DevtoolsUtils.js';
8
+ import { UncaughtError } from '../PageCollector.js';
9
+ import * as DevTools from '../third_party/index.js';
7
10
  export class ConsoleFormatter {
8
- #msg;
9
- #resolvedArgs = [];
10
- #resolvedStackTrace;
11
+ static #STACK_TRACE_MAX_LINES = 50;
11
12
  #id;
12
- constructor(msg, options) {
13
- this.#msg = msg;
14
- this.#id = options?.id;
15
- this.#resolvedStackTrace = options?.resolvedStackTraceForTesting;
13
+ #type;
14
+ #text;
15
+ #argCount;
16
+ #resolvedArgs;
17
+ #stack;
18
+ #cause;
19
+ #isIgnored;
20
+ constructor(params) {
21
+ this.#id = params.id;
22
+ this.#type = params.type;
23
+ this.#text = params.text;
24
+ this.#argCount = params.argCount ?? 0;
25
+ this.#resolvedArgs = params.resolvedArgs ?? [];
26
+ this.#stack = params.stack;
27
+ this.#cause = params.cause;
28
+ this.#isIgnored = params.isIgnored;
16
29
  }
17
30
  static async from(msg, options) {
18
- const formatter = new ConsoleFormatter(msg, options);
19
- if (options?.fetchDetailedData) {
20
- await formatter.#loadDetailedData(options?.devTools);
31
+ const ignoreListManager = options?.devTools?.universe.context.get(DevTools.DevTools.IgnoreListManager);
32
+ const isIgnored = options.isIgnoredForTesting ||
33
+ (frame => {
34
+ if (!ignoreListManager) {
35
+ return false;
36
+ }
37
+ if (frame.uiSourceCode) {
38
+ return ignoreListManager.isUserOrSourceMapIgnoreListedUISourceCode(frame.uiSourceCode);
39
+ }
40
+ if (frame.url) {
41
+ return ignoreListManager.isUserIgnoreListedURL(frame.url);
42
+ }
43
+ return false;
44
+ });
45
+ if (msg instanceof UncaughtError) {
46
+ const error = await SymbolizedError.fromDetails({
47
+ devTools: options?.devTools,
48
+ details: msg.details,
49
+ targetId: msg.targetId,
50
+ includeStackAndCause: options?.fetchDetailedData,
51
+ resolvedStackTraceForTesting: options?.resolvedStackTraceForTesting,
52
+ resolvedCauseForTesting: options?.resolvedCauseForTesting,
53
+ });
54
+ return new _a({
55
+ id: options.id,
56
+ type: 'error',
57
+ text: error.message,
58
+ stack: error.stackTrace,
59
+ cause: error.cause,
60
+ isIgnored,
61
+ });
21
62
  }
22
- return formatter;
23
- }
24
- #isConsoleMessage(msg) {
25
- // No `instanceof` as tests mock `ConsoleMessage`.
26
- return 'args' in msg && typeof msg.args === 'function';
27
- }
28
- async #loadDetailedData(devTools) {
29
- if (this.#msg instanceof Error) {
30
- return;
63
+ let resolvedArgs = [];
64
+ if (options.resolvedArgsForTesting) {
65
+ resolvedArgs = options.resolvedArgsForTesting;
31
66
  }
32
- if (this.#isConsoleMessage(this.#msg)) {
33
- this.#resolvedArgs = await Promise.all(this.#msg.args().map(async (arg, i) => {
67
+ else if (options.fetchDetailedData) {
68
+ resolvedArgs = await Promise.all(msg.args().map(async (arg, i) => {
34
69
  try {
70
+ const remoteObject = arg.remoteObject();
71
+ if (remoteObject.type === 'object' &&
72
+ remoteObject.subtype === 'error') {
73
+ return await SymbolizedError.fromError({
74
+ devTools: options.devTools,
75
+ error: remoteObject,
76
+ // @ts-expect-error Internal ConsoleMessage API
77
+ targetId: msg._targetId(),
78
+ });
79
+ }
35
80
  return await arg.jsonValue();
36
81
  }
37
82
  catch {
@@ -39,71 +84,66 @@ export class ConsoleFormatter {
39
84
  }
40
85
  }));
41
86
  }
42
- if (devTools) {
87
+ let stack;
88
+ if (options.resolvedStackTraceForTesting) {
89
+ stack = options.resolvedStackTraceForTesting;
90
+ }
91
+ else if (options.fetchDetailedData && options.devTools) {
43
92
  try {
44
- if (this.#isConsoleMessage(this.#msg)) {
45
- this.#resolvedStackTrace = await createStackTraceForConsoleMessage(devTools, this.#msg);
46
- }
47
- else if (this.#msg.stackTrace) {
48
- this.#resolvedStackTrace = await createStackTrace(devTools, this.#msg.stackTrace, this.#msg.targetId);
49
- }
93
+ stack = await createStackTraceForConsoleMessage(options.devTools, msg);
50
94
  }
51
95
  catch {
52
96
  // ignore
53
97
  }
54
98
  }
99
+ return new _a({
100
+ id: options.id,
101
+ type: msg.type(),
102
+ text: msg.text(),
103
+ argCount: resolvedArgs.length || msg.args().length,
104
+ resolvedArgs,
105
+ stack,
106
+ isIgnored,
107
+ });
55
108
  }
56
109
  // The short format for a console message.
57
110
  toString() {
58
- const type = this.#getType();
59
- const text = this.#getText();
60
- const argsCount = this.#getArgsCount();
61
- const idPart = this.#id !== undefined ? `msgid=${this.#id} ` : '';
62
- return `${idPart}[${type}] ${text} (${argsCount} args)`;
111
+ return `msgid=${this.#id} [${this.#type}] ${this.#text} (${this.#argCount} args)`;
63
112
  }
64
113
  // The verbose format for a console message, including all details.
65
114
  toStringDetailed() {
66
115
  const result = [
67
- this.#id !== undefined ? `ID: ${this.#id}` : '',
68
- `Message: ${this.#getType()}> ${this.#getText()}`,
116
+ `ID: ${this.#id}`,
117
+ `Message: ${this.#type}> ${this.#text}`,
69
118
  this.#formatArgs(),
70
- this.#formatStackTrace(this.#resolvedStackTrace),
119
+ this.#formatStackTrace(this.#stack, this.#cause, {
120
+ includeHeading: true,
121
+ }),
71
122
  ].filter(line => !!line);
72
123
  return result.join('\n');
73
124
  }
74
- #getType() {
75
- if (!this.#isConsoleMessage(this.#msg)) {
76
- return 'error';
77
- }
78
- return this.#msg.type();
79
- }
80
- #getText() {
81
- if (!this.#isConsoleMessage(this.#msg)) {
82
- return this.#msg.message;
83
- }
84
- return this.#msg.text();
85
- }
86
125
  #getArgs() {
87
- if (!this.#isConsoleMessage(this.#msg)) {
88
- return [];
89
- }
90
126
  if (this.#resolvedArgs.length > 0) {
91
127
  const args = [...this.#resolvedArgs];
92
128
  // If there is no text, the first argument serves as text (see formatMessage).
93
- if (!this.#msg.text()) {
129
+ if (!this.#text) {
94
130
  args.shift();
95
131
  }
96
132
  return args;
97
133
  }
98
134
  return [];
99
135
  }
100
- #getArgsCount() {
101
- if (!this.#isConsoleMessage(this.#msg)) {
102
- return 0;
103
- }
104
- return this.#resolvedArgs.length || this.#msg.args().length;
105
- }
106
136
  #formatArg(arg) {
137
+ if (arg instanceof SymbolizedError) {
138
+ return [
139
+ arg.message,
140
+ this.#formatStackTrace(arg.stackTrace, arg.cause, {
141
+ includeHeading: false,
142
+ }),
143
+ ]
144
+ .filter(line => !!line)
145
+ .join('\n');
146
+ }
107
147
  return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
108
148
  }
109
149
  #formatArgs() {
@@ -117,25 +157,47 @@ export class ConsoleFormatter {
117
157
  }
118
158
  return result.join('\n');
119
159
  }
120
- #formatStackTrace(stackTrace) {
160
+ #formatStackTrace(stackTrace, cause, opts) {
121
161
  if (!stackTrace) {
122
162
  return '';
123
163
  }
164
+ const lines = this.#formatStackTraceInner(stackTrace, cause);
165
+ const includedLines = lines.slice(0, _a.#STACK_TRACE_MAX_LINES);
166
+ const reminderCount = lines.length - includedLines.length;
124
167
  return [
125
- '### Stack trace',
126
- this.#formatFragment(stackTrace.syncFragment),
127
- ...stackTrace.asyncFragments.map(this.#formatAsyncFragment.bind(this)),
168
+ opts.includeHeading ? '### Stack trace' : '',
169
+ ...includedLines,
170
+ reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
128
171
  'Note: line and column numbers use 1-based indexing',
129
- ].join('\n');
172
+ ]
173
+ .filter(line => !!line)
174
+ .join('\n');
175
+ }
176
+ #formatStackTraceInner(stackTrace, cause) {
177
+ if (!stackTrace) {
178
+ return [];
179
+ }
180
+ return [
181
+ ...this.#formatFragment(stackTrace.syncFragment),
182
+ ...stackTrace.asyncFragments
183
+ .map(this.#formatAsyncFragment.bind(this))
184
+ .flat(),
185
+ ...this.#formatCause(cause),
186
+ ];
130
187
  }
131
188
  #formatFragment(fragment) {
132
- return fragment.frames.map(this.#formatFrame.bind(this)).join('\n');
189
+ const frames = fragment.frames.filter(frame => !this.#isIgnored(frame));
190
+ return frames.map(this.#formatFrame.bind(this));
133
191
  }
134
192
  #formatAsyncFragment(fragment) {
193
+ const formattedFrames = this.#formatFragment(fragment);
194
+ if (formattedFrames.length === 0) {
195
+ return [];
196
+ }
135
197
  const separatorLineLength = 40;
136
198
  const prefix = `--- ${fragment.description || 'async'} `;
137
199
  const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
138
- return separator + '\n' + this.#formatFragment(fragment);
200
+ return [separator, ...formattedFrames];
139
201
  }
140
202
  #formatFrame(frame) {
141
203
  let result = `at ${frame.name ?? '<anonymous>'}`;
@@ -148,21 +210,31 @@ export class ConsoleFormatter {
148
210
  }
149
211
  return result;
150
212
  }
213
+ #formatCause(cause) {
214
+ if (!cause) {
215
+ return [];
216
+ }
217
+ return [
218
+ `Caused by: ${cause.message}`,
219
+ ...this.#formatStackTraceInner(cause.stackTrace, cause.cause),
220
+ ];
221
+ }
151
222
  toJSON() {
152
223
  return {
153
- type: this.#getType(),
154
- text: this.#getText(),
155
- argsCount: this.#getArgsCount(),
224
+ type: this.#type,
225
+ text: this.#text,
226
+ argsCount: this.#argCount,
156
227
  id: this.#id,
157
228
  };
158
229
  }
159
230
  toJSONDetailed() {
160
231
  return {
161
232
  id: this.#id,
162
- type: this.#getType(),
163
- text: this.#getText(),
233
+ type: this.#type,
234
+ text: this.#text,
164
235
  args: this.#getArgs().map(arg => typeof arg === 'object' ? arg : String(arg)),
165
- stackTrace: this.#resolvedStackTrace,
236
+ stackTrace: this.#stack,
166
237
  };
167
238
  }
168
239
  }
240
+ _a = ConsoleFormatter;
package/build/src/main.js CHANGED
@@ -12,15 +12,15 @@ import { logger, saveLogsToFile } from './logger.js';
12
12
  import { McpContext } from './McpContext.js';
13
13
  import { McpResponse } from './McpResponse.js';
14
14
  import { Mutex } from './Mutex.js';
15
- import { ClearcutLogger } from './telemetry/clearcut-logger.js';
16
- import { computeFlagUsage } from './telemetry/flag-utils.js';
17
- import { bucketizeLatency } from './telemetry/metric-utils.js';
15
+ import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
16
+ import { computeFlagUsage } from './telemetry/flagUtils.js';
17
+ import { bucketizeLatency } from './telemetry/metricUtils.js';
18
18
  import { McpServer, StdioServerTransport, SetLevelRequestSchema, } from './third_party/index.js';
19
19
  import { ToolCategory } from './tools/categories.js';
20
20
  import { tools } from './tools/tools.js';
21
21
  // If moved update release-please config
22
22
  // x-release-please-start-version
23
- const VERSION = '0.16.0';
23
+ const VERSION = '0.17.1';
24
24
  // x-release-please-end
25
25
  export const args = parseArguments(VERSION);
26
26
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
@@ -87,6 +87,7 @@ async function getContext() {
87
87
  context = await McpContext.from(browser, logger, {
88
88
  experimentalDevToolsDebugging: devtools,
89
89
  experimentalIncludeAllPages: args.experimentalIncludeAllPages,
90
+ performanceCrux: args.performanceCrux,
90
91
  });
91
92
  }
92
93
  return context;
@@ -95,6 +96,9 @@ const logDisclaimers = () => {
95
96
  console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
96
97
  debug, and modify any data in the browser or DevTools.
97
98
  Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
99
+ if (args.performanceCrux) {
100
+ console.error(`Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data. To disable, run with --no-performance-crux.`);
101
+ }
98
102
  if (args.usageStatistics) {
99
103
  console.error(`
100
104
  Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
@@ -7,7 +7,7 @@ import process from 'node:process';
7
7
  import { logger } from '../logger.js';
8
8
  import { FilePersistence } from './persistence.js';
9
9
  import { WatchdogMessageType, OsType } from './types.js';
10
- import { WatchdogClient } from './watchdog-client.js';
10
+ import { WatchdogClient } from './WatchdogClient.js';
11
11
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
12
12
  function detectOsType() {
13
13
  switch (process.platform) {
@@ -8,7 +8,7 @@ import readline from 'node:readline';
8
8
  import { parseArgs } from 'node:util';
9
9
  import { logger, flushLogs, saveLogsToFile } from '../../logger.js';
10
10
  import { WatchdogMessageType } from '../types.js';
11
- import { ClearcutSender } from './clearcut-sender.js';
11
+ import { ClearcutSender } from './ClearcutSender.js';
12
12
  function parseWatchdogArgs() {
13
13
  const { values } = parseArgs({
14
14
  options: {