chrome-devtools-mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@ MCP clients.
27
27
 
28
28
  ## Requirements
29
29
 
30
- - [Node.js 22.12.0](https://nodejs.org/) or newer.
30
+ - [Node.js 20](https://nodejs.org/) or a newer [latest maintainance LTS](https://github.com/nodejs/Release#release-schedule) version.
31
31
  - [Chrome](https://www.google.com/chrome/) current stable version or newer.
32
32
  - [npm](https://www.npmjs.com/).
33
33
 
@@ -75,6 +75,23 @@ claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
75
75
  codex mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
76
76
  ```
77
77
 
78
+ **On Windows 11**
79
+
80
+ Configure the Chrome install location and increase the startup timeout by updating `.codex/config.toml` and adding the following `env` and `startup_timeout_ms` parameters:
81
+
82
+ ```
83
+ [mcp_servers.chrome-devtools]
84
+ command = "cmd"
85
+ args = [
86
+ "/c",
87
+ "npx",
88
+ "-y",
89
+ "chrome-devtools-mcp@latest",
90
+ ]
91
+ env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" }
92
+ startup_timeout_ms = 20_000
93
+ ```
94
+
78
95
  </details>
79
96
 
80
97
  <details>
@@ -102,8 +119,22 @@ Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided ab
102
119
 
103
120
  <details>
104
121
  <summary>Gemini CLI</summary>
105
- Follow the <a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server">MCP guide</a>
106
- using the standard config from above.
122
+ Install the Chrome DevTools MCP server using the Gemini CLI.
123
+
124
+ **Project wide:**
125
+
126
+ ```bash
127
+ gemini mcp add chrome-devtools npx chrome-devtools-mcp@latest
128
+ ```
129
+
130
+ **Globally:**
131
+
132
+ ```bash
133
+ gemini mcp add -s user chrome-devtools npx chrome-devtools-mcp@latest
134
+ ```
135
+
136
+ Alternatively, follow the <a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server">MCP guide</a> and use the standard config from above.
137
+
107
138
  </details>
108
139
 
109
140
  <details>
@@ -201,6 +232,10 @@ The Chrome DevTools MCP server supports the following configuration option:
201
232
  - **Type:** string
202
233
  - **Choices:** `stable`, `canary`, `beta`, `dev`
203
234
 
235
+ - **`--logFile`**
236
+ Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
237
+ - **Type:** string
238
+
204
239
  <!-- END AUTO GENERATED OPTIONS -->
205
240
 
206
241
  Pass them via the `args` property in the JSON configuration. For example:
@@ -19,7 +19,7 @@ export var SubscriptionTier;
19
19
  SubscriptionTier["PRO_ANNUAL"] = "SUBSCRIPTION_TIER_PRO_ANNUAL";
20
20
  SubscriptionTier["PRO_MONTHLY"] = "SUBSCRIPTION_TIER_PRO_MONTHLY";
21
21
  })(SubscriptionTier || (SubscriptionTier = {}));
22
- var EligibilityStatus;
22
+ export var EligibilityStatus;
23
23
  (function (EligibilityStatus) {
24
24
  EligibilityStatus["ELIGIBLE"] = "ELIGIBLE";
25
25
  EligibilityStatus["NOT_ELIGIBLE"] = "NOT_ELIGIBLE";
@@ -65,12 +65,18 @@ export class GdpClient {
65
65
  return gdpClientInstance;
66
66
  }
67
67
  async initialize() {
68
- return await Promise.all([this.getProfile(), this.checkEligibility()]).then(([profile, eligibilityResponse]) => {
68
+ const profile = await this.getProfile();
69
+ if (profile) {
69
70
  return {
70
- hasProfile: Boolean(profile),
71
- isEligible: eligibilityResponse?.createProfile === EligibilityStatus.ELIGIBLE
71
+ hasProfile: true,
72
+ isEligible: true,
72
73
  };
73
- });
74
+ }
75
+ const isEligible = await this.isEligibleToCreateProfile();
76
+ return {
77
+ hasProfile: false,
78
+ isEligible,
79
+ };
74
80
  }
75
81
  async getProfile() {
76
82
  if (this.#cachedProfilePromise) {
@@ -81,7 +87,11 @@ export class GdpClient {
81
87
  path: '/v1beta1/profile:get',
82
88
  method: 'GET',
83
89
  });
84
- return await this.#cachedProfilePromise;
90
+ const profile = await this.#cachedProfilePromise;
91
+ if (profile) {
92
+ this.#cachedEligibilityPromise = Promise.resolve({ createProfile: EligibilityStatus.ELIGIBLE });
93
+ }
94
+ return profile;
85
95
  }
86
96
  async checkEligibility() {
87
97
  if (this.#cachedEligibilityPromise) {
@@ -27,20 +27,12 @@ function getLCPData(parsedTrace, frameId, navigationId) {
27
27
  metricScore: metric,
28
28
  };
29
29
  }
30
- export class PerformanceInsightFormatter extends PerformanceTraceFormatter {
30
+ export class PerformanceInsightFormatter {
31
+ #traceFormatter;
31
32
  #insight;
32
33
  #parsedTrace;
33
- /**
34
- * A utility method because we dependency inject this formatter into
35
- * PerformanceTraceFormatter; this allows you to pass
36
- * PerformanceInsightFormatter.create rather than an anonymous
37
- * function that wraps the constructor.
38
- */
39
- static create(focus, insight) {
40
- return new PerformanceInsightFormatter(focus, insight);
41
- }
42
34
  constructor(focus, insight) {
43
- super(focus, null);
35
+ this.#traceFormatter = new PerformanceTraceFormatter(focus);
44
36
  this.#insight = insight;
45
37
  this.#parsedTrace = focus.parsedTrace;
46
38
  }
@@ -57,8 +49,7 @@ export class PerformanceInsightFormatter extends PerformanceTraceFormatter {
57
49
  return this.#formatMilli(Trace.Helpers.Timing.microToMilli(x));
58
50
  }
59
51
  #formatRequestUrl(request) {
60
- const eventKey = this.eventsSerializer.keyForEvent(request);
61
- return `${request.args.data.url} (eventKey: ${eventKey})`;
52
+ return `${request.args.data.url} ${this.#traceFormatter.serializeEvent(request)}`;
62
53
  }
63
54
  #formatScriptUrl(script) {
64
55
  if (script.request) {
@@ -97,7 +88,7 @@ export class PerformanceInsightFormatter extends PerformanceTraceFormatter {
97
88
  ];
98
89
  if (lcpRequest) {
99
90
  parts.push(`${theLcpElement} is an image fetched from ${this.#formatRequestUrl(lcpRequest)}.`);
100
- const request = this.formatNetworkRequests([lcpRequest], { verbose: true, customTitle: 'LCP resource network request' });
91
+ const request = this.#traceFormatter.formatNetworkRequests([lcpRequest], { verbose: true, customTitle: 'LCP resource network request' });
101
92
  parts.push(request);
102
93
  }
103
94
  else {
@@ -305,7 +296,7 @@ ${shiftsFormatted.join('\n')}`;
305
296
  });
306
297
  return `${this.#lcpMetricSharedContext()}
307
298
 
308
- ${this.formatNetworkRequests([documentRequest], {
299
+ ${this.#traceFormatter.formatNetworkRequests([documentRequest], {
309
300
  verbose: true,
310
301
  customTitle: 'Document network request'
311
302
  })}
@@ -580,8 +571,8 @@ ${filesFormatted}`;
580
571
  */
581
572
  formatModernHttpInsight(insight) {
582
573
  const requestSummary = (insight.http1Requests.length === 1) ?
583
- this.formatNetworkRequests(insight.http1Requests, { verbose: true }) :
584
- this.formatNetworkRequests(insight.http1Requests);
574
+ this.#traceFormatter.formatNetworkRequests(insight.http1Requests, { verbose: true }) :
575
+ this.#traceFormatter.formatNetworkRequests(insight.http1Requests);
585
576
  if (requestSummary.length === 0) {
586
577
  return 'There are no requests that were served over a legacy HTTP protocol.';
587
578
  }
@@ -665,7 +656,7 @@ ${requestSummary}`;
665
656
  * @returns a string formatted for sending to Ask AI.
666
657
  */
667
658
  formatRenderBlockingInsight(insight) {
668
- const requestSummary = this.formatNetworkRequests(insight.renderBlockingRequests);
659
+ const requestSummary = this.#traceFormatter.formatNetworkRequests(insight.renderBlockingRequests);
669
660
  if (requestSummary.length === 0) {
670
661
  return 'There are no network requests that are render blocking.';
671
662
  }
@@ -856,7 +847,7 @@ ${this.#links()}`;
856
847
  #links() {
857
848
  switch (this.#insight.insightKey) {
858
849
  case 'CLSCulprits':
859
- return `- https://wdeb.dev/articles/cls
850
+ return `- https://web.dev/articles/cls
860
851
  - https://web.dev/articles/optimize-cls`;
861
852
  case 'DocumentLatency':
862
853
  return '- https://web.dev/articles/optimize-ttfb';
@@ -5,28 +5,21 @@ import * as CrUXManager from '../../crux-manager/crux-manager.js';
5
5
  import * as Trace from '../../trace/trace.js';
6
6
  import { AIQueries } from '../performance/AIQueries.js';
7
7
  import { NetworkRequestFormatter } from './NetworkRequestFormatter.js';
8
+ import { PerformanceInsightFormatter } from './PerformanceInsightFormatter.js';
8
9
  import { bytes, micros, millis } from './UnitFormatters.js';
9
10
  export class PerformanceTraceFormatter {
10
11
  #focus;
11
12
  #parsedTrace;
12
13
  #insightSet;
13
- #getInsightFormatter = null;
14
- eventsSerializer;
15
- /**
16
- * We inject the insight formatter because otherwise we get a circular
17
- * dependency between PerformanceInsightFormatter and
18
- * PerformanceTraceFormatter. This is OK in the browser build, but breaks when
19
- * we reuse this code in NodeJS for DevTools MCP.
20
- */
21
- constructor(focus, getInsightFormatter) {
14
+ #eventsSerializer;
15
+ constructor(focus) {
22
16
  this.#focus = focus;
23
17
  this.#parsedTrace = focus.parsedTrace;
24
18
  this.#insightSet = focus.insightSet;
25
- this.eventsSerializer = focus.eventsSerializer;
26
- this.#getInsightFormatter = getInsightFormatter;
19
+ this.#eventsSerializer = focus.eventsSerializer;
27
20
  }
28
21
  serializeEvent(event) {
29
- const key = this.eventsSerializer.keyForEvent(event);
22
+ const key = this.#eventsSerializer.keyForEvent(event);
30
23
  return `(eventKey: ${key}, ts: ${event.ts})`;
31
24
  }
32
25
  serializeBounds(bounds) {
@@ -155,10 +148,7 @@ export class PerformanceTraceFormatter {
155
148
  if (model.state === 'pass') {
156
149
  continue;
157
150
  }
158
- const formatter = this.#getInsightFormatter?.(this.#focus, model);
159
- if (!formatter) {
160
- continue;
161
- }
151
+ const formatter = new PerformanceInsightFormatter(this.#focus, model);
162
152
  if (!formatter.insightIsSupported()) {
163
153
  continue;
164
154
  }
@@ -455,7 +445,7 @@ export class PerformanceTraceFormatter {
455
445
  });
456
446
  const initiators = this.#getInitiatorChain(parsedTrace, request);
457
447
  const initiatorUrls = initiators.map(initiator => initiator.args.data.url);
458
- const eventKey = this.eventsSerializer.keyForEvent(request);
448
+ const eventKey = this.#eventsSerializer.keyForEvent(request);
459
449
  const eventKeyLine = eventKey ? `eventKey: ${eventKey}\n` : '';
460
450
  return `${titlePrefix}: ${url}
461
451
  ${eventKeyLine}Timings:
@@ -504,6 +494,29 @@ Network requests data:
504
494
  .join(', ')}]`;
505
495
  return networkDataString + '\n\n' + urlsMapString + '\n\n' + allRequestsText;
506
496
  }
497
+ static callFrameDataFormatDescription = `Each call frame is presented in the following format:
498
+
499
+ 'id;eventKey;name;duration;selfTime;urlIndex;childRange;[S]'
500
+
501
+ Key definitions:
502
+
503
+ * id: A unique numerical identifier for the call frame. Never mention this id in the output to the user.
504
+ * eventKey: String that uniquely identifies this event in the flame chart.
505
+ * name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
506
+ * duration: The total execution time of the call frame, including its children.
507
+ * selfTime: The time spent directly within the call frame, excluding its children's execution.
508
+ * urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
509
+ * childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
510
+ * S: _Optional_. The letter 'S' terminates the line if that call frame was selected by the user.
511
+
512
+ Example Call Tree:
513
+
514
+ 1;r-123;main;500;100;;
515
+ 2;r-124;update;200;50;;3
516
+ 3;p-49575-15428179-2834-374;animate;150;20;0;4-5;S
517
+ 4;p-49575-15428179-3505-1162;calculatePosition;80;80;;
518
+ 5;p-49575-15428179-5391-2767;applyStyles;50;50;;
519
+ `;
507
520
  /**
508
521
  * Network requests format description that is sent to the model as a fact.
509
522
  */
@@ -575,7 +588,7 @@ The order of headers corresponds to an internal fixed list. If a header is not p
575
588
  const initiatorUrlIndices = initiators.map(initiator => this.#getOrAssignUrlIndex(urlIdToIndex, initiator.args.data.url));
576
589
  const parts = [
577
590
  urlIndex,
578
- this.eventsSerializer.keyForEvent(request) ?? '',
591
+ this.#eventsSerializer.keyForEvent(request) ?? '',
579
592
  queuedTime,
580
593
  requestSentTime,
581
594
  downloadCompleteTime,
@@ -1,38 +1,2 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.LegacyJavaScript = void 0;
37
- const LegacyJavaScript = __importStar(require("./lib/legacy-javascript.js"));
38
- exports.LegacyJavaScript = LegacyJavaScript;
1
+ import * as LegacyJavaScript from './lib/legacy-javascript.js';
2
+ export { LegacyJavaScript };
@@ -1,8 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectLegacyJavaScript = detectLegacyJavaScript;
4
- exports.getCoreJsPolyfillData = getCoreJsPolyfillData;
5
- exports.getTransformPatterns = getTransformPatterns;
6
1
  // core/lib/legacy-javascript/polyfill-module-data.json
7
2
  var polyfill_module_data_default = [
8
3
  {
@@ -937,6 +932,7 @@ function detectLegacyJavaScript(content, map) {
937
932
  estimatedByteSavings: estimateWastedBytes(content, matches)
938
933
  };
939
934
  }
935
+ export { detectLegacyJavaScript, getCoreJsPolyfillData, getTransformPatterns };
940
936
  /**
941
937
  * @license
942
938
  * Copyright 2025 Google LLC
@@ -1,5 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
1
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
2
  var __commonJS = (cb, mod) => function __require() {
5
3
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
@@ -145,4 +143,4 @@ var require_nostats_subset = __commonJS({
145
143
  module.exports = require_nostats();
146
144
  }
147
145
  });
148
- exports.default = require_nostats_subset();
146
+ export default require_nostats_subset();
@@ -1,8 +1,2 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ThirdPartyWeb = void 0;
7
- const nostats_subset_js_1 = __importDefault(require("./lib/nostats-subset.js"));
8
- exports.ThirdPartyWeb = nostats_subset_js_1.default;
1
+ import ThirdPartyWeb from './lib/nostats-subset.js';
2
+ export { ThirdPartyWeb };
@@ -8,6 +8,7 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { NetworkCollector, PageCollector } from './PageCollector.js';
10
10
  import { listPages } from './tools/pages.js';
11
+ import { takeSnapshot } from './tools/snapshot.js';
11
12
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
12
13
  import { WaitForHelper } from './WaitForHelper.js';
13
14
  const DEFAULT_TIMEOUT = 5_000;
@@ -191,7 +192,7 @@ export class McpContext {
191
192
  }
192
193
  async getElementByUid(uid) {
193
194
  if (!this.#textSnapshot?.idToNode.size) {
194
- throw new Error('No snapshot found. Use browser_snapshot to capture one');
195
+ throw new Error(`No snapshot found. Use ${takeSnapshot.name} to capture one.`);
195
196
  }
196
197
  const [snapshotId] = uid.split('_');
197
198
  if (this.#textSnapshot.snapshotId !== snapshotId) {
@@ -1,17 +1,17 @@
1
1
  import { formatConsoleEvent } from './formatters/consoleFormatter.js';
2
2
  import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
3
3
  import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
4
+ import { handleDialog } from './tools/pages.js';
4
5
  import { paginate } from './utils/pagination.js';
5
6
  export class McpResponse {
6
7
  #includePages = false;
7
8
  #includeSnapshot = false;
8
- #includeNetworkRequests = false;
9
9
  #attachedNetworkRequestUrl;
10
10
  #includeConsoleData = false;
11
11
  #textResponseLines = [];
12
12
  #formattedConsoleData;
13
13
  #images = [];
14
- #networkRequestsPaginationOptions;
14
+ #networkRequestsOptions;
15
15
  setIncludePages(value) {
16
16
  this.#includePages = value;
17
17
  }
@@ -19,14 +19,19 @@ export class McpResponse {
19
19
  this.#includeSnapshot = value;
20
20
  }
21
21
  setIncludeNetworkRequests(value, options) {
22
- this.#includeNetworkRequests = value;
23
- if (!value || !options) {
24
- this.#networkRequestsPaginationOptions = undefined;
22
+ if (!value) {
23
+ this.#networkRequestsOptions = undefined;
25
24
  return;
26
25
  }
27
- this.#networkRequestsPaginationOptions = {
28
- pageSize: options.pageSize,
29
- pageIdx: options.pageIdx,
26
+ this.#networkRequestsOptions = {
27
+ include: value,
28
+ pagination: options?.pageSize || options?.pageIdx
29
+ ? {
30
+ pageSize: options.pageSize,
31
+ pageIdx: options.pageIdx,
32
+ }
33
+ : undefined,
34
+ resourceTypes: options?.resourceTypes,
30
35
  };
31
36
  }
32
37
  setIncludeConsoleData(value) {
@@ -39,7 +44,7 @@ export class McpResponse {
39
44
  return this.#includePages;
40
45
  }
41
46
  get includeNetworkRequests() {
42
- return this.#includeNetworkRequests;
47
+ return this.#networkRequestsOptions?.include ?? false;
43
48
  }
44
49
  get includeConsoleData() {
45
50
  return this.#includeConsoleData;
@@ -48,7 +53,7 @@ export class McpResponse {
48
53
  return this.#attachedNetworkRequestUrl;
49
54
  }
50
55
  get networkRequestsPageIdx() {
51
- return this.#networkRequestsPaginationOptions?.pageIdx;
56
+ return this.#networkRequestsOptions?.pagination?.pageIdx;
52
57
  }
53
58
  appendResponseLine(value) {
54
59
  this.#textResponseLines.push(value);
@@ -102,7 +107,7 @@ export class McpResponse {
102
107
  if (dialog) {
103
108
  response.push(`# Open dialog
104
109
  ${dialog.type()}: ${dialog.message()} (default value: ${dialog.message()}).
105
- Call browser_handle_dialog to handle it before continuing.`);
110
+ Call ${handleDialog.name} to handle it before continuing.`);
106
111
  }
107
112
  if (this.#includePages) {
108
113
  const parts = [`## Pages`];
@@ -122,17 +127,25 @@ Call browser_handle_dialog to handle it before continuing.`);
122
127
  }
123
128
  }
124
129
  response.push(...this.#getIncludeNetworkRequestsData(context));
125
- if (this.#includeNetworkRequests) {
126
- const requests = context.getNetworkRequests();
130
+ if (this.#networkRequestsOptions?.include) {
131
+ let requests = context.getNetworkRequests();
132
+ // Apply resource type filtering if specified
133
+ if (this.#networkRequestsOptions.resourceTypes?.length) {
134
+ const normalizedTypes = new Set(this.#networkRequestsOptions.resourceTypes);
135
+ requests = requests.filter(request => {
136
+ const type = request.resourceType();
137
+ return normalizedTypes.has(type);
138
+ });
139
+ }
127
140
  response.push('## Network requests');
128
141
  if (requests.length) {
129
- const paginationResult = paginate(requests, this.#networkRequestsPaginationOptions);
142
+ const paginationResult = paginate(requests, this.#networkRequestsOptions.pagination);
130
143
  if (paginationResult.invalidPage) {
131
144
  response.push('Invalid page number provided. Showing first page.');
132
145
  }
133
146
  const { startIndex, endIndex, currentPage, totalPages } = paginationResult;
134
147
  response.push(`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`);
135
- if (this.#networkRequestsPaginationOptions) {
148
+ if (this.#networkRequestsOptions.pagination) {
136
149
  if (paginationResult.hasNextPage) {
137
150
  response.push(`Next page: ${currentPage + 1}`);
138
151
  }
package/build/src/cli.js CHANGED
@@ -46,8 +46,7 @@ export const cliOptions = {
46
46
  },
47
47
  logFile: {
48
48
  type: 'string',
49
- describe: 'Save the logs to file.',
50
- hidden: true,
49
+ describe: 'Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.',
51
50
  },
52
51
  };
53
52
  export function parseArguments(version, argv = process.argv) {
@@ -4,10 +4,14 @@
4
4
  * Copyright 2025 Google LLC
5
5
  * SPDX-License-Identifier: Apache-2.0
6
6
  */
7
- const [major, minor] = process.version.substring(1).split('.').map(Number);
8
- if (major < 22 || (major === 22 && minor < 12)) {
9
- console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22.12.0 or newer.`);
7
+ import { version } from 'node:process';
8
+ const [major, minor] = version.substring(1).split('.').map(Number);
9
+ if (major === 22 && minor < 12) {
10
+ console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22.12.0 LTS or a newer LTS.`);
11
+ process.exit(1);
12
+ }
13
+ if (major < 20) {
14
+ console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20 LTS or a newer LTS.`);
10
15
  process.exit(1);
11
16
  }
12
17
  await import('./main.js');
13
- export {};
package/build/src/main.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import './polyfill.js';
6
7
  import assert from 'node:assert';
7
8
  import fs from 'node:fs';
8
9
  import path from 'node:path';
@@ -70,7 +71,7 @@ async function getContext() {
70
71
  const logDisclaimers = () => {
71
72
  console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
72
73
  debug, and modify any data in the browser or DevTools.
73
- Avoid sharing sensitive or personal information that you do want to share with MCP clients.`);
74
+ Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
74
75
  };
75
76
  const toolMutex = new Mutex();
76
77
  function registerTool(tool) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google Inc.
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import 'core-js/modules/es.promise.with-resolvers.js';
7
+ import 'core-js/proposals/iterator-helpers.js';
@@ -6,6 +6,27 @@
6
6
  import z from 'zod';
7
7
  import { ToolCategories } from './categories.js';
8
8
  import { defineTool } from './ToolDefinition.js';
9
+ const FILTERABLE_RESOURCE_TYPES = [
10
+ 'document',
11
+ 'stylesheet',
12
+ 'image',
13
+ 'media',
14
+ 'font',
15
+ 'script',
16
+ 'texttrack',
17
+ 'xhr',
18
+ 'fetch',
19
+ 'prefetch',
20
+ 'eventsource',
21
+ 'websocket',
22
+ 'manifest',
23
+ 'signedexchange',
24
+ 'ping',
25
+ 'cspviolationreport',
26
+ 'preflight',
27
+ 'fedcm',
28
+ 'other',
29
+ ];
9
30
  export const listNetworkRequests = defineTool({
10
31
  name: 'list_network_requests',
11
32
  description: `List all requests for the currently selected page`,
@@ -26,11 +47,16 @@ export const listNetworkRequests = defineTool({
26
47
  .min(0)
27
48
  .optional()
28
49
  .describe('Page number to return (0-based). When omitted, returns the first page.'),
50
+ resourceTypes: z
51
+ .array(z.enum(FILTERABLE_RESOURCE_TYPES))
52
+ .optional()
53
+ .describe('Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.'),
29
54
  },
30
55
  handler: async (request, response) => {
31
56
  response.setIncludeNetworkRequests(true, {
32
57
  pageSize: request.params.pageSize,
33
58
  pageIdx: request.params.pageIdx,
59
+ resourceTypes: request.params.resourceTypes,
34
60
  });
35
61
  },
36
62
  });
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import z from 'zod';
7
+ import { logger } from '../logger.js';
7
8
  import { ToolCategories } from './categories.js';
8
9
  import { CLOSE_PAGE_ERROR, defineTool } from './ToolDefinition.js';
9
10
  export const listPages = defineTool({
@@ -172,12 +173,24 @@ export const handleDialog = defineTool({
172
173
  }
173
174
  switch (request.params.action) {
174
175
  case 'accept': {
175
- await dialog.accept(request.params.promptText);
176
+ try {
177
+ await dialog.accept(request.params.promptText);
178
+ }
179
+ catch (err) {
180
+ // Likely already handled by the user outside of MCP.
181
+ logger(err);
182
+ }
176
183
  response.appendResponseLine('Successfully accepted the dialog');
177
184
  break;
178
185
  }
179
186
  case 'dismiss': {
180
- await dialog.dismiss();
187
+ try {
188
+ await dialog.dismiss();
189
+ }
190
+ catch (err) {
191
+ // Likely already handled.
192
+ logger(err);
193
+ }
181
194
  response.appendResponseLine('Successfully dismissed the dialog');
182
195
  break;
183
196
  }
@@ -10,7 +10,7 @@ import { ToolCategories } from './categories.js';
10
10
  import { defineTool } from './ToolDefinition.js';
11
11
  export const startTrace = defineTool({
12
12
  name: 'performance_start_trace',
13
- description: 'Starts a performance trace recording on the selected page.',
13
+ description: 'Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.',
14
14
  annotations: {
15
15
  category: ToolCategories.PERFORMANCE,
16
16
  readOnlyHint: true,
@@ -18,7 +18,7 @@ export const startTrace = defineTool({
18
18
  schema: {
19
19
  reload: z
20
20
  .boolean()
21
- .describe('Determines if, once tracing has started, the page should be automatically reloaded'),
21
+ .describe('Determines if, once tracing has started, the page should be automatically reloaded.'),
22
22
  autoStop: z
23
23
  .boolean()
24
24
  .describe('Determines if the trace recording should be automatically stopped.'),
@@ -18,6 +18,12 @@ export const screenshot = defineTool({
18
18
  .enum(['png', 'jpeg'])
19
19
  .default('png')
20
20
  .describe('Type of format to save the screenshot as. Default is "png"'),
21
+ quality: z
22
+ .number()
23
+ .min(0)
24
+ .max(100)
25
+ .optional()
26
+ .describe('Compression quality for JPEG format (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
21
27
  uid: z
22
28
  .string()
23
29
  .optional()
@@ -41,6 +47,8 @@ export const screenshot = defineTool({
41
47
  const screenshot = await pageOrHandle.screenshot({
42
48
  type: request.params.format,
43
49
  fullPage: request.params.fullPage,
50
+ quality: request.params.quality,
51
+ optimizeForSpeed: true, // Bonus: optimize encoding for speed
44
52
  });
45
53
  if (request.params.uid) {
46
54
  response.appendResponseLine(`Took a screenshot of node with uid "${request.params.uid}".`);
@@ -49,11 +49,19 @@ export async function parseRawTraceBuffer(buffer) {
49
49
  };
50
50
  }
51
51
  }
52
+ const extraFormatDescriptions = `Information on performance traces may contain main thread activity represented as call frames and network requests.
53
+
54
+ ${PerformanceTraceFormatter.callFrameDataFormatDescription}
55
+
56
+ ${PerformanceTraceFormatter.networkDataFormatDescription}
57
+ `;
52
58
  export function getTraceSummary(result) {
53
59
  const focus = AgentFocus.fromParsedTrace(result.parsedTrace);
54
- const formatter = new PerformanceTraceFormatter(focus, PerformanceInsightFormatter.create);
60
+ const formatter = new PerformanceTraceFormatter(focus);
55
61
  const output = formatter.formatTraceSummary();
56
- return output;
62
+ return `${extraFormatDescriptions}
63
+
64
+ ${output}`;
57
65
  }
58
66
  export function getInsightOutput(result, insightName) {
59
67
  if (!result.insights) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": "./build/src/index.js",
@@ -14,6 +14,7 @@
14
14
  "docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
15
15
  "start": "npm run build && node build/src/index.js",
16
16
  "start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
17
+ "test:node20": "node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test build/tests",
17
18
  "test": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test \"build/tests/**/*.test.js\"",
18
19
  "test:only": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
19
20
  "test:only:no-build": "node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
@@ -36,7 +37,8 @@
36
37
  "homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp#readme",
37
38
  "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp",
38
39
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "1.18.1",
40
+ "@modelcontextprotocol/sdk": "1.18.2",
41
+ "core-js": "3.45.1",
40
42
  "debug": "4.4.3",
41
43
  "puppeteer-core": "24.22.3",
42
44
  "yargs": "18.0.0"
@@ -51,16 +53,16 @@
51
53
  "@types/yargs": "^17.0.33",
52
54
  "@typescript-eslint/eslint-plugin": "^8.43.0",
53
55
  "@typescript-eslint/parser": "^8.43.0",
54
- "chrome-devtools-frontend": "1.0.1520139",
56
+ "chrome-devtools-frontend": "1.0.1520535",
55
57
  "eslint": "^9.35.0",
56
- "eslint-plugin-import": "^2.32.0",
57
58
  "eslint-import-resolver-typescript": "^4.4.4",
59
+ "eslint-plugin-import": "^2.32.0",
58
60
  "globals": "^16.4.0",
59
61
  "prettier": "^3.6.2",
60
62
  "puppeteer": "24.22.3",
61
63
  "sinon": "^21.0.0",
62
- "typescript-eslint": "^8.43.0",
63
- "typescript": "^5.9.2"
64
+ "typescript": "^5.9.2",
65
+ "typescript-eslint": "^8.43.0"
64
66
  },
65
67
  "engines": {
66
68
  "node": ">=22.12.0"