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 +38 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +16 -6
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -19
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +31 -18
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/legacy-javascript.js +2 -38
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/lib/legacy-javascript.js +1 -5
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/lib/nostats-subset.js +1 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/third-party-web.js +2 -8
- package/build/src/McpContext.js +2 -1
- package/build/src/McpResponse.js +28 -15
- package/build/src/cli.js +1 -2
- package/build/src/index.js +8 -4
- package/build/src/main.js +2 -1
- package/build/src/polyfill.js +7 -0
- package/build/src/tools/network.js +26 -0
- package/build/src/tools/pages.js +15 -2
- package/build/src/tools/performance.js +2 -2
- package/build/src/tools/screenshot.js +8 -0
- package/build/src/trace-processing/parse.js +10 -2
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ MCP clients.
|
|
|
27
27
|
|
|
28
28
|
## Requirements
|
|
29
29
|
|
|
30
|
-
- [Node.js
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
68
|
+
const profile = await this.getProfile();
|
|
69
|
+
if (profile) {
|
|
69
70
|
return {
|
|
70
|
-
hasProfile:
|
|
71
|
-
isEligible:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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://
|
|
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
|
-
#
|
|
14
|
-
|
|
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
|
|
26
|
-
this.#getInsightFormatter = getInsightFormatter;
|
|
19
|
+
this.#eventsSerializer = focus.eventsSerializer;
|
|
27
20
|
}
|
|
28
21
|
serializeEvent(event) {
|
|
29
|
-
const key = this
|
|
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 =
|
|
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
|
|
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
|
|
591
|
+
this.#eventsSerializer.keyForEvent(request) ?? '',
|
|
579
592
|
queuedTime,
|
|
580
593
|
requestSentTime,
|
|
581
594
|
downloadCompleteTime,
|
|
@@ -1,38 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
146
|
+
export default require_nostats_subset();
|
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 };
|
package/build/src/McpContext.js
CHANGED
|
@@ -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(
|
|
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) {
|
package/build/src/McpResponse.js
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
this.#networkRequestsPaginationOptions = undefined;
|
|
22
|
+
if (!value) {
|
|
23
|
+
this.#networkRequestsOptions = undefined;
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
|
-
this.#
|
|
28
|
-
|
|
29
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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.#
|
|
126
|
-
|
|
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.#
|
|
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.#
|
|
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: '
|
|
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) {
|
package/build/src/index.js
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* Copyright 2025 Google LLC
|
|
5
5
|
* SPDX-License-Identifier: Apache-2.0
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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) {
|
|
@@ -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
|
});
|
package/build/src/tools/pages.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
60
|
+
const formatter = new PerformanceTraceFormatter(focus);
|
|
55
61
|
const output = formatter.formatTraceSummary();
|
|
56
|
-
return
|
|
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
|
+
"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.
|
|
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.
|
|
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
|
|
63
|
-
"typescript": "^
|
|
64
|
+
"typescript": "^5.9.2",
|
|
65
|
+
"typescript-eslint": "^8.43.0"
|
|
64
66
|
},
|
|
65
67
|
"engines": {
|
|
66
68
|
"node": ">=22.12.0"
|