chrome-devtools-mcp 0.5.0 → 0.6.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 +50 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHost.js +3 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +6 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSRule.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSStartingStyle.js +21 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +19 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/sdk.js +2 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +3 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScriptsHandler.js +2 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LegacyJavaScript.js +2 -1
- package/build/src/McpContext.js +10 -3
- package/build/src/McpResponse.js +1 -1
- package/build/src/PageCollector.js +17 -6
- package/build/src/browser.js +15 -9
- package/build/src/cli.js +29 -0
- package/build/src/index.js +5 -1
- package/build/src/logger.js +2 -2
- package/build/src/main.js +18 -10
- package/build/src/tools/ToolDefinition.js +11 -0
- package/build/src/tools/input.js +1 -1
- package/build/src/tools/network.js +0 -1
- package/build/src/tools/pages.js +15 -5
- package/build/src/tools/screenshot.js +12 -3
- package/build/src/tools/snapshot.js +11 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ control and inspect a live Chrome browser. It acts as a Model-Context-Protocol
|
|
|
7
7
|
(MCP) server, giving your AI coding assistant access to the full power of
|
|
8
8
|
Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.
|
|
9
9
|
|
|
10
|
+
## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md)
|
|
11
|
+
|
|
10
12
|
## Key features
|
|
11
13
|
|
|
12
14
|
- **Get performance insights**: Uses [Chrome
|
|
@@ -40,7 +42,7 @@ Add the following config to your MCP client:
|
|
|
40
42
|
"mcpServers": {
|
|
41
43
|
"chrome-devtools": {
|
|
42
44
|
"command": "npx",
|
|
43
|
-
"args": ["chrome-devtools-mcp@latest"]
|
|
45
|
+
"args": ["-y", "chrome-devtools-mcp@latest"]
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -94,6 +96,30 @@ startup_timeout_ms = 20_000
|
|
|
94
96
|
|
|
95
97
|
</details>
|
|
96
98
|
|
|
99
|
+
<details>
|
|
100
|
+
<summary>Copilot CLI</summary>
|
|
101
|
+
|
|
102
|
+
Start Copilot CLI:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
copilot
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Start the dialog to add a new MCP server by running:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
/mcp add
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Configure the following fields and press `CTR-S` to save the configuration:
|
|
115
|
+
|
|
116
|
+
- **Server name:** `chrome-devtools`
|
|
117
|
+
- **Server Type:** `[1] Local`
|
|
118
|
+
- **Command:** `npx`
|
|
119
|
+
- **Arguments:** `-y, chrome-devtools-mcp@latest`
|
|
120
|
+
|
|
121
|
+
</details>
|
|
122
|
+
|
|
97
123
|
<details>
|
|
98
124
|
<summary>Copilot / VS Code</summary>
|
|
99
125
|
Follow the MCP install <a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server">guide</a>,
|
|
@@ -109,7 +135,7 @@ startup_timeout_ms = 20_000
|
|
|
109
135
|
|
|
110
136
|
**Click the button to install:**
|
|
111
137
|
|
|
112
|
-
[<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=chrome-devtools&config=
|
|
138
|
+
[<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=chrome-devtools&config=eyJjb21tYW5kIjoibnB4IC15IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D)
|
|
113
139
|
|
|
114
140
|
**Or install manually:**
|
|
115
141
|
|
|
@@ -151,6 +177,14 @@ The same way chrome-devtools-mcp can be configured for JetBrains Junie in `Setti
|
|
|
151
177
|
|
|
152
178
|
</details>
|
|
153
179
|
|
|
180
|
+
<details>
|
|
181
|
+
<summary>Visual Studio</summary>
|
|
182
|
+
|
|
183
|
+
**Click the button to install:**
|
|
184
|
+
|
|
185
|
+
[<img src="https://img.shields.io/badge/Visual_Studio-Install-C16FDE?logo=visualstudio&logoColor=white" alt="Install in Visual Studio">](https://vs-open.link/mcp-install?%7B%22name%22%3A%22chrome-devtools%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22chrome-devtools-mcp%40latest%22%5D%7D)
|
|
186
|
+
</details>
|
|
187
|
+
|
|
154
188
|
### Your first prompt
|
|
155
189
|
|
|
156
190
|
Enter the following prompt in your MCP Client to check if everything is working:
|
|
@@ -166,6 +200,8 @@ Your MCP client should open the browser and record a performance trace.
|
|
|
166
200
|
|
|
167
201
|
## Tools
|
|
168
202
|
|
|
203
|
+
If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md).
|
|
204
|
+
|
|
169
205
|
<!-- BEGIN AUTO GENERATED TOOLS -->
|
|
170
206
|
|
|
171
207
|
- **Input automation** (7 tools)
|
|
@@ -236,6 +272,18 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
236
272
|
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
273
|
- **Type:** string
|
|
238
274
|
|
|
275
|
+
- **`--viewport`**
|
|
276
|
+
Initial viewport size for the Chromee instances started by the server. For example, `1280x720`
|
|
277
|
+
- **Type:** string
|
|
278
|
+
|
|
279
|
+
- **`--proxyServer`**
|
|
280
|
+
Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.
|
|
281
|
+
- **Type:** string
|
|
282
|
+
|
|
283
|
+
- **`--acceptInsecureCerts`**
|
|
284
|
+
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
|
|
285
|
+
- **Type:** boolean
|
|
286
|
+
|
|
239
287
|
<!-- END AUTO GENERATED OPTIONS -->
|
|
240
288
|
|
|
241
289
|
Pass them via the `args` property in the JSON configuration. For example:
|
package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHost.js
CHANGED
|
@@ -301,6 +301,9 @@ export class InspectorFrontendHostStub {
|
|
|
301
301
|
devToolsFlexibleLayout: {
|
|
302
302
|
verticalDrawerEnabled: true,
|
|
303
303
|
},
|
|
304
|
+
devToolsStartingStyleDebugging: {
|
|
305
|
+
enabled: false,
|
|
306
|
+
},
|
|
304
307
|
};
|
|
305
308
|
if ('hostConfigForTesting' in globalThis) {
|
|
306
309
|
const { hostConfigForTesting } = globalThis;
|
|
@@ -368,6 +368,11 @@ export class CSSModel extends SDKModel {
|
|
|
368
368
|
null;
|
|
369
369
|
return new InlineStyleResult(inlineStyle, attributesStyle);
|
|
370
370
|
}
|
|
371
|
+
forceStartingStyle(node, forced) {
|
|
372
|
+
void this.agent.invoke_forceStartingStyle({ nodeId: node.id, forced });
|
|
373
|
+
this.dispatchEventToListeners(Events.StartingStylesStateForced, node);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
371
376
|
forcePseudoState(node, pseudoClass, enable) {
|
|
372
377
|
const forcedPseudoClasses = node.marker(PseudoStateMarker) || [];
|
|
373
378
|
const hasPseudoClass = forcedPseudoClasses.includes(pseudoClass);
|
|
@@ -781,6 +786,7 @@ export var Events;
|
|
|
781
786
|
Events["ModelWasEnabled"] = "ModelWasEnabled";
|
|
782
787
|
Events["ModelDisposed"] = "ModelDisposed";
|
|
783
788
|
Events["PseudoStateForced"] = "PseudoStateForced";
|
|
789
|
+
Events["StartingStylesStateForced"] = "StartingStylesStateForced";
|
|
784
790
|
Events["StyleSheetAdded"] = "StyleSheetAdded";
|
|
785
791
|
Events["StyleSheetChanged"] = "StyleSheetChanged";
|
|
786
792
|
Events["StyleSheetRemoved"] = "StyleSheetRemoved";
|
|
@@ -7,6 +7,7 @@ import { CSSContainerQuery } from './CSSContainerQuery.js';
|
|
|
7
7
|
import { CSSLayer } from './CSSLayer.js';
|
|
8
8
|
import { CSSMedia } from './CSSMedia.js';
|
|
9
9
|
import { CSSScope } from './CSSScope.js';
|
|
10
|
+
import { CSSStartingStyle } from './CSSStartingStyle.js';
|
|
10
11
|
import { CSSStyleDeclaration, Type } from './CSSStyleDeclaration.js';
|
|
11
12
|
import { CSSSupports } from './CSSSupports.js';
|
|
12
13
|
function styleSheetHeaderForRule(cssModel, { styleSheetId }) {
|
|
@@ -83,6 +84,7 @@ export class CSSStyleRule extends CSSRule {
|
|
|
83
84
|
scopes;
|
|
84
85
|
layers;
|
|
85
86
|
ruleTypes;
|
|
87
|
+
startingStyles;
|
|
86
88
|
wasUsed;
|
|
87
89
|
constructor(cssModel, payload, wasUsed) {
|
|
88
90
|
super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload) });
|
|
@@ -95,6 +97,8 @@ export class CSSStyleRule extends CSSRule {
|
|
|
95
97
|
this.scopes = payload.scopes ? CSSScope.parseScopesPayload(cssModel, payload.scopes) : [];
|
|
96
98
|
this.supports = payload.supports ? CSSSupports.parseSupportsPayload(cssModel, payload.supports) : [];
|
|
97
99
|
this.layers = payload.layers ? CSSLayer.parseLayerPayload(cssModel, payload.layers) : [];
|
|
100
|
+
this.startingStyles =
|
|
101
|
+
payload.startingStyles ? CSSStartingStyle.parseStartingStylePayload(cssModel, payload.startingStyles) : [];
|
|
98
102
|
this.ruleTypes = payload.ruleTypes || [];
|
|
99
103
|
this.wasUsed = wasUsed || false;
|
|
100
104
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright 2025 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
|
5
|
+
import { CSSQuery } from './CSSQuery.js';
|
|
6
|
+
export class CSSStartingStyle extends CSSQuery {
|
|
7
|
+
static parseStartingStylePayload(cssModel, payload) {
|
|
8
|
+
return payload.map(p => new CSSStartingStyle(cssModel, p));
|
|
9
|
+
}
|
|
10
|
+
constructor(cssModel, payload) {
|
|
11
|
+
super(cssModel);
|
|
12
|
+
this.reinitialize(payload);
|
|
13
|
+
}
|
|
14
|
+
reinitialize(payload) {
|
|
15
|
+
this.range = payload.range ? TextUtils.TextRange.TextRange.fromObject(payload.range) : null;
|
|
16
|
+
this.styleSheetId = payload.styleSheetId;
|
|
17
|
+
}
|
|
18
|
+
active() {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -88,6 +88,7 @@ export class DOMNode {
|
|
|
88
88
|
#xmlVersion;
|
|
89
89
|
#isSVGNode;
|
|
90
90
|
#isScrollable;
|
|
91
|
+
#affectedByStartingStyles;
|
|
91
92
|
#creationStackTrace = null;
|
|
92
93
|
#pseudoElements = new Map();
|
|
93
94
|
#distributedNodes = [];
|
|
@@ -148,6 +149,7 @@ export class DOMNode {
|
|
|
148
149
|
this.#xmlVersion = payload.xmlVersion;
|
|
149
150
|
this.#isSVGNode = Boolean(payload.isSVG);
|
|
150
151
|
this.#isScrollable = Boolean(payload.isScrollable);
|
|
152
|
+
this.#affectedByStartingStyles = Boolean(payload.affectedByStartingStyles);
|
|
151
153
|
this.#retainedNodes = retainedNodes;
|
|
152
154
|
if (this.#retainedNodes?.has(this.backendNodeId())) {
|
|
153
155
|
this.retained = true;
|
|
@@ -237,6 +239,9 @@ export class DOMNode {
|
|
|
237
239
|
isScrollable() {
|
|
238
240
|
return this.#isScrollable;
|
|
239
241
|
}
|
|
242
|
+
affectedByStartingStyles() {
|
|
243
|
+
return this.#affectedByStartingStyles;
|
|
244
|
+
}
|
|
240
245
|
isMediaNode() {
|
|
241
246
|
return this.#nodeName === 'AUDIO' || this.#nodeName === 'VIDEO';
|
|
242
247
|
}
|
|
@@ -279,6 +284,9 @@ export class DOMNode {
|
|
|
279
284
|
setIsScrollable(isScrollable) {
|
|
280
285
|
this.#isScrollable = isScrollable;
|
|
281
286
|
}
|
|
287
|
+
setAffectedByStartingStyles(affectedByStartingStyles) {
|
|
288
|
+
this.#affectedByStartingStyles = affectedByStartingStyles;
|
|
289
|
+
}
|
|
282
290
|
hasAttributes() {
|
|
283
291
|
return this.#attributes.size > 0;
|
|
284
292
|
}
|
|
@@ -1327,6 +1335,14 @@ export class DOMModel extends SDKModel {
|
|
|
1327
1335
|
node.setIsScrollable(isScrollable);
|
|
1328
1336
|
this.dispatchEventToListeners(Events.ScrollableFlagUpdated, { node });
|
|
1329
1337
|
}
|
|
1338
|
+
affectedByStartingStylesFlagUpdated(nodeId, affectedByStartingStyles) {
|
|
1339
|
+
const node = this.nodeForId(nodeId);
|
|
1340
|
+
if (!node || node.affectedByStartingStyles() === affectedByStartingStyles) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
node.setAffectedByStartingStyles(affectedByStartingStyles);
|
|
1344
|
+
this.dispatchEventToListeners(Events.AffectedByStartingStylesFlagUpdated, { node });
|
|
1345
|
+
}
|
|
1330
1346
|
topLayerElementsUpdated() {
|
|
1331
1347
|
this.dispatchEventToListeners(Events.TopLayerElementsChanged);
|
|
1332
1348
|
}
|
|
@@ -1478,6 +1494,7 @@ export var Events;
|
|
|
1478
1494
|
Events["MarkersChanged"] = "MarkersChanged";
|
|
1479
1495
|
Events["TopLayerElementsChanged"] = "TopLayerElementsChanged";
|
|
1480
1496
|
Events["ScrollableFlagUpdated"] = "ScrollableFlagUpdated";
|
|
1497
|
+
Events["AffectedByStartingStylesFlagUpdated"] = "AffectedByStartingStylesFlagUpdated";
|
|
1481
1498
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
1482
1499
|
})(Events || (Events = {}));
|
|
1483
1500
|
class DOMDispatcher {
|
|
@@ -1533,7 +1550,8 @@ class DOMDispatcher {
|
|
|
1533
1550
|
scrollableFlagUpdated({ nodeId, isScrollable }) {
|
|
1534
1551
|
this.#domModel.scrollableFlagUpdated(nodeId, isScrollable);
|
|
1535
1552
|
}
|
|
1536
|
-
affectedByStartingStylesFlagUpdated(
|
|
1553
|
+
affectedByStartingStylesFlagUpdated({ nodeId, affectedByStartingStyles }) {
|
|
1554
|
+
this.#domModel.affectedByStartingStylesFlagUpdated(nodeId, affectedByStartingStyles);
|
|
1537
1555
|
}
|
|
1538
1556
|
}
|
|
1539
1557
|
let domModelUndoStackInstance = null;
|
package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js
CHANGED
|
@@ -164,7 +164,11 @@ class RehydratingSessionBase {
|
|
|
164
164
|
this.connection = connection;
|
|
165
165
|
}
|
|
166
166
|
sendMessageToFrontend(payload) {
|
|
167
|
-
|
|
167
|
+
// The frontend doesn't expect CDP responses within the same synchronous event loop, so it breaks unexpectedly.
|
|
168
|
+
// Any async boundary will do, so we use setTimeout.
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
this.connection?.postToFrontend(payload);
|
|
171
|
+
});
|
|
168
172
|
}
|
|
169
173
|
handleFrontendMessageAsFakeCDPAgent(data) {
|
|
170
174
|
// Send default response in default session.
|
|
@@ -33,6 +33,7 @@ import * as CSSPropertyParserMatchers from './CSSPropertyParserMatchers.js';
|
|
|
33
33
|
import * as CSSQuery from './CSSQuery.js';
|
|
34
34
|
import * as CSSRule from './CSSRule.js';
|
|
35
35
|
import * as CSSScope from './CSSScope.js';
|
|
36
|
+
import * as CSSStartingStyle from './CSSStartingStyle.js';
|
|
36
37
|
import * as CSSStyleDeclaration from './CSSStyleDeclaration.js';
|
|
37
38
|
import * as CSSStyleSheetHeader from './CSSStyleSheetHeader.js';
|
|
38
39
|
import * as CSSSupports from './CSSSupports.js';
|
|
@@ -85,5 +86,5 @@ import * as Target from './Target.js';
|
|
|
85
86
|
import * as TargetManager from './TargetManager.js';
|
|
86
87
|
import * as TraceObject from './TraceObject.js';
|
|
87
88
|
import * as WebAuthnModel from './WebAuthnModel.js';
|
|
88
|
-
export { AccessibilityModel, AnimationModel, AutofillModel, CategorizedBreakpoint, ChildTargetManager, CompilerSourceMappingContentProvider, Connections, ConsoleModel, Cookie, CookieModel, CookieParser, CPUProfilerModel, CPUThrottlingManager, CSSContainerQuery, CSSFontFace, CSSLayer, CSSMatchedStyles, CSSMedia, CSSMetadata, CSSModel, CSSProperty, CSSPropertyParser, CSSPropertyParserMatchers, CSSQuery, CSSRule, CSSScope, CSSStyleDeclaration, CSSStyleSheetHeader, CSSSupports, DebuggerModel, DOMDebuggerModel, DOMModel, EmulationModel, EnhancedTracesParser, EventBreakpointsModel, FrameAssociated, FrameManager, HeapProfilerModel, IOModel, IsolateManager, IssuesModel, LayerTreeBase, LogModel, NetworkManager, NetworkRequest, OverlayColorGenerator, OverlayModel, OverlayPersistentHighlighter, PageLoad, PageResourceLoader, PaintProfiler, PerformanceMetricsModel, PreloadingModel, RehydratingConnection, // TODO(crbug.com/444191656): Exported for tests.
|
|
89
|
+
export { AccessibilityModel, AnimationModel, AutofillModel, CategorizedBreakpoint, ChildTargetManager, CompilerSourceMappingContentProvider, Connections, ConsoleModel, Cookie, CookieModel, CookieParser, CPUProfilerModel, CPUThrottlingManager, CSSContainerQuery, CSSFontFace, CSSLayer, CSSMatchedStyles, CSSMedia, CSSMetadata, CSSModel, CSSProperty, CSSPropertyParser, CSSPropertyParserMatchers, CSSQuery, CSSRule, CSSScope, CSSStartingStyle, CSSStyleDeclaration, CSSStyleSheetHeader, CSSSupports, DebuggerModel, DOMDebuggerModel, DOMModel, EmulationModel, EnhancedTracesParser, EventBreakpointsModel, FrameAssociated, FrameManager, HeapProfilerModel, IOModel, IsolateManager, IssuesModel, LayerTreeBase, LogModel, NetworkManager, NetworkRequest, OverlayColorGenerator, OverlayModel, OverlayPersistentHighlighter, PageLoad, PageResourceLoader, PaintProfiler, PerformanceMetricsModel, PreloadingModel, RehydratingConnection, // TODO(crbug.com/444191656): Exported for tests.
|
|
89
90
|
RemoteObject, Resource, ResourceTreeModel, RuntimeModel, ScreenCaptureModel, Script, SDKModel, SecurityOriginManager, ServerSentEventProtocol, ServerTiming, ServiceWorkerCacheModel, ServiceWorkerManager, SourceMap, SourceMapCache, SourceMapFunctionRanges, SourceMapManager, SourceMapScopeChainEntry, SourceMapScopesInfo, StorageBucketsModel, StorageKeyManager, Target, TargetManager, TraceObject, WebAuthnModel, };
|
package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js
CHANGED
|
@@ -286,7 +286,7 @@ export function registerCommands(inspectorBackend) {
|
|
|
286
286
|
inspectorBackend.registerType("CSS.Specificity", [{ "name": "a", "type": "number", "optional": false, "description": "The a component, which represents the number of ID selectors.", "typeRef": null }, { "name": "b", "type": "number", "optional": false, "description": "The b component, which represents the number of class selectors, attributes selectors, and pseudo-classes.", "typeRef": null }, { "name": "c", "type": "number", "optional": false, "description": "The c component, which represents the number of type selectors and pseudo-elements.", "typeRef": null }]);
|
|
287
287
|
inspectorBackend.registerType("CSS.SelectorList", [{ "name": "selectors", "type": "array", "optional": false, "description": "Selectors in the list.", "typeRef": "CSS.Value" }, { "name": "text", "type": "string", "optional": false, "description": "Rule selector text.", "typeRef": null }]);
|
|
288
288
|
inspectorBackend.registerType("CSS.CSSStyleSheetHeader", [{ "name": "styleSheetId", "type": "string", "optional": false, "description": "The stylesheet identifier.", "typeRef": "CSS.StyleSheetId" }, { "name": "frameId", "type": "string", "optional": false, "description": "Owner frame identifier.", "typeRef": "Page.FrameId" }, { "name": "sourceURL", "type": "string", "optional": false, "description": "Stylesheet resource URL. Empty if this is a constructed stylesheet created using new CSSStyleSheet() (but non-empty if this is a constructed stylesheet imported as a CSS module script).", "typeRef": null }, { "name": "sourceMapURL", "type": "string", "optional": true, "description": "URL of source map associated with the stylesheet (if any).", "typeRef": null }, { "name": "origin", "type": "string", "optional": false, "description": "Stylesheet origin.", "typeRef": "CSS.StyleSheetOrigin" }, { "name": "title", "type": "string", "optional": false, "description": "Stylesheet title.", "typeRef": null }, { "name": "ownerNode", "type": "number", "optional": true, "description": "The backend id for the owner node of the stylesheet.", "typeRef": "DOM.BackendNodeId" }, { "name": "disabled", "type": "boolean", "optional": false, "description": "Denotes whether the stylesheet is disabled.", "typeRef": null }, { "name": "hasSourceURL", "type": "boolean", "optional": true, "description": "Whether the sourceURL field value comes from the sourceURL comment.", "typeRef": null }, { "name": "isInline", "type": "boolean", "optional": false, "description": "Whether this stylesheet is created for STYLE tag by parser. This flag is not set for document.written STYLE tags.", "typeRef": null }, { "name": "isMutable", "type": "boolean", "optional": false, "description": "Whether this stylesheet is mutable. Inline stylesheets become mutable after they have been modified via CSSOM API. `<link>` element's stylesheets become mutable only if DevTools modifies them. Constructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.", "typeRef": null }, { "name": "isConstructed", "type": "boolean", "optional": false, "description": "True if this stylesheet is created through new CSSStyleSheet() or imported as a CSS module script.", "typeRef": null }, { "name": "startLine", "type": "number", "optional": false, "description": "Line offset of the stylesheet within the resource (zero based).", "typeRef": null }, { "name": "startColumn", "type": "number", "optional": false, "description": "Column offset of the stylesheet within the resource (zero based).", "typeRef": null }, { "name": "length", "type": "number", "optional": false, "description": "Size of the content (in characters).", "typeRef": null }, { "name": "endLine", "type": "number", "optional": false, "description": "Line offset of the end of the stylesheet within the resource (zero based).", "typeRef": null }, { "name": "endColumn", "type": "number", "optional": false, "description": "Column offset of the end of the stylesheet within the resource (zero based).", "typeRef": null }, { "name": "loadingFailed", "type": "boolean", "optional": true, "description": "If the style sheet was loaded from a network resource, this indicates when the resource failed to load", "typeRef": null }]);
|
|
289
|
-
inspectorBackend.registerType("CSS.CSSRule", [{ "name": "styleSheetId", "type": "string", "optional": true, "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified stylesheet rules) this rule came from.", "typeRef": "CSS.StyleSheetId" }, { "name": "selectorList", "type": "object", "optional": false, "description": "Rule selector data.", "typeRef": "CSS.SelectorList" }, { "name": "nestingSelectors", "type": "array", "optional": true, "description": "Array of selectors from ancestor style rules, sorted by distance from the current rule.", "typeRef": "string" }, { "name": "origin", "type": "string", "optional": false, "description": "Parent stylesheet's origin.", "typeRef": "CSS.StyleSheetOrigin" }, { "name": "style", "type": "object", "optional": false, "description": "Associated style declaration.", "typeRef": "CSS.CSSStyle" }, { "name": "media", "type": "array", "optional": true, "description": "Media list array (for rules involving media queries). The array enumerates media queries starting with the innermost one, going outwards.", "typeRef": "CSS.CSSMedia" }, { "name": "containerQueries", "type": "array", "optional": true, "description": "Container query list array (for rules involving container queries). The array enumerates container queries starting with the innermost one, going outwards.", "typeRef": "CSS.CSSContainerQuery" }, { "name": "supports", "type": "array", "optional": true, "description": "@supports CSS at-rule array. The array enumerates @supports at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSSupports" }, { "name": "layers", "type": "array", "optional": true, "description": "Cascade layer array. Contains the layer hierarchy that this rule belongs to starting with the innermost layer and going outwards.", "typeRef": "CSS.CSSLayer" }, { "name": "scopes", "type": "array", "optional": true, "description": "@scope CSS at-rule array. The array enumerates @scope at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSScope" }, { "name": "ruleTypes", "type": "array", "optional": true, "description": "The array keeps the types of ancestor CSSRules from the innermost going outwards.", "typeRef": "CSS.CSSRuleType" }, { "name": "startingStyles", "type": "array", "optional": true, "description": "@starting-style CSS at-rule array. The array enumerates @starting-style at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSStartingStyle" }]);
|
|
289
|
+
inspectorBackend.registerType("CSS.CSSRule", [{ "name": "styleSheetId", "type": "string", "optional": true, "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified stylesheet rules) this rule came from.", "typeRef": "CSS.StyleSheetId" }, { "name": "selectorList", "type": "object", "optional": false, "description": "Rule selector data.", "typeRef": "CSS.SelectorList" }, { "name": "nestingSelectors", "type": "array", "optional": true, "description": "Array of selectors from ancestor style rules, sorted by distance from the current rule.", "typeRef": "string" }, { "name": "origin", "type": "string", "optional": false, "description": "Parent stylesheet's origin.", "typeRef": "CSS.StyleSheetOrigin" }, { "name": "style", "type": "object", "optional": false, "description": "Associated style declaration.", "typeRef": "CSS.CSSStyle" }, { "name": "originTreeScopeNodeId", "type": "number", "optional": true, "description": "The BackendNodeId of the DOM node that constitutes the origin tree scope of this rule.", "typeRef": "DOM.BackendNodeId" }, { "name": "media", "type": "array", "optional": true, "description": "Media list array (for rules involving media queries). The array enumerates media queries starting with the innermost one, going outwards.", "typeRef": "CSS.CSSMedia" }, { "name": "containerQueries", "type": "array", "optional": true, "description": "Container query list array (for rules involving container queries). The array enumerates container queries starting with the innermost one, going outwards.", "typeRef": "CSS.CSSContainerQuery" }, { "name": "supports", "type": "array", "optional": true, "description": "@supports CSS at-rule array. The array enumerates @supports at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSSupports" }, { "name": "layers", "type": "array", "optional": true, "description": "Cascade layer array. Contains the layer hierarchy that this rule belongs to starting with the innermost layer and going outwards.", "typeRef": "CSS.CSSLayer" }, { "name": "scopes", "type": "array", "optional": true, "description": "@scope CSS at-rule array. The array enumerates @scope at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSScope" }, { "name": "ruleTypes", "type": "array", "optional": true, "description": "The array keeps the types of ancestor CSSRules from the innermost going outwards.", "typeRef": "CSS.CSSRuleType" }, { "name": "startingStyles", "type": "array", "optional": true, "description": "@starting-style CSS at-rule array. The array enumerates @starting-style at-rules starting with the innermost one, going outwards.", "typeRef": "CSS.CSSStartingStyle" }]);
|
|
290
290
|
inspectorBackend.registerType("CSS.RuleUsage", [{ "name": "styleSheetId", "type": "string", "optional": false, "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified stylesheet rules) this rule came from.", "typeRef": "CSS.StyleSheetId" }, { "name": "startOffset", "type": "number", "optional": false, "description": "Offset of the start of the rule (including selector) from the beginning of the stylesheet.", "typeRef": null }, { "name": "endOffset", "type": "number", "optional": false, "description": "Offset of the end of the rule body from the beginning of the stylesheet.", "typeRef": null }, { "name": "used", "type": "boolean", "optional": false, "description": "Indicates whether the rule was actually used by some element in the page.", "typeRef": null }]);
|
|
291
291
|
inspectorBackend.registerType("CSS.SourceRange", [{ "name": "startLine", "type": "number", "optional": false, "description": "Start line of range.", "typeRef": null }, { "name": "startColumn", "type": "number", "optional": false, "description": "Start column of range (inclusive).", "typeRef": null }, { "name": "endLine", "type": "number", "optional": false, "description": "End line of range", "typeRef": null }, { "name": "endColumn", "type": "number", "optional": false, "description": "End column of range (exclusive).", "typeRef": null }]);
|
|
292
292
|
inspectorBackend.registerType("CSS.ShorthandEntry", [{ "name": "name", "type": "string", "optional": false, "description": "Shorthand name.", "typeRef": null }, { "name": "value", "type": "string", "optional": false, "description": "Shorthand value.", "typeRef": null }, { "name": "important", "type": "boolean", "optional": true, "description": "Whether the property has \\\"!important\\\" annotation (implies `false` if absent).", "typeRef": null }]);
|
|
@@ -1221,7 +1221,8 @@ export function registerCommands(inspectorBackend) {
|
|
|
1221
1221
|
inspectorBackend.registerEvent("Storage.attributionReportingTriggerRegistered", ["registration", "eventLevel", "aggregatable"]);
|
|
1222
1222
|
inspectorBackend.registerEvent("Storage.attributionReportingReportSent", ["url", "body", "result", "netError", "netErrorName", "httpStatusCode"]);
|
|
1223
1223
|
inspectorBackend.registerEvent("Storage.attributionReportingVerboseDebugReportSent", ["url", "body", "netError", "netErrorName", "httpStatusCode"]);
|
|
1224
|
-
inspectorBackend.registerCommand("Storage.getStorageKeyForFrame", [{ "name": "frameId", "type": "string", "optional": false, "description": "", "typeRef": "Page.FrameId" }], ["storageKey"], "Returns a storage key given a frame id.");
|
|
1224
|
+
inspectorBackend.registerCommand("Storage.getStorageKeyForFrame", [{ "name": "frameId", "type": "string", "optional": false, "description": "", "typeRef": "Page.FrameId" }], ["storageKey"], "Returns a storage key given a frame id. Deprecated. Please use Storage.getStorageKey instead.");
|
|
1225
|
+
inspectorBackend.registerCommand("Storage.getStorageKey", [{ "name": "frameId", "type": "string", "optional": true, "description": "", "typeRef": "Page.FrameId" }], ["storageKey"], "Returns storage key for the given frame. If no frame ID is provided, the storage key of the target executing this command is returned.");
|
|
1225
1226
|
inspectorBackend.registerCommand("Storage.clearDataForOrigin", [{ "name": "origin", "type": "string", "optional": false, "description": "Security origin.", "typeRef": null }, { "name": "storageTypes", "type": "string", "optional": false, "description": "Comma separated list of StorageType to clear.", "typeRef": null }], [], "Clears storage for origin.");
|
|
1226
1227
|
inspectorBackend.registerCommand("Storage.clearDataForStorageKey", [{ "name": "storageKey", "type": "string", "optional": false, "description": "Storage key.", "typeRef": null }, { "name": "storageTypes", "type": "string", "optional": false, "description": "Comma separated list of StorageType to clear.", "typeRef": null }], [], "Clears storage for storage key.");
|
|
1227
1228
|
inspectorBackend.registerCommand("Storage.getCookies", [{ "name": "browserContextId", "type": "string", "optional": true, "description": "Browser context to use when called on the browser endpoint.", "typeRef": "Browser.BrowserContextID" }], ["cookies"], "Returns all browser cookies.");
|
|
@@ -17,7 +17,7 @@ export function handleEvent(event) {
|
|
|
17
17
|
const getOrMakeScript = (isolate, scriptIdAsNumber) => {
|
|
18
18
|
const scriptId = String(scriptIdAsNumber);
|
|
19
19
|
const key = `${isolate}.${scriptId}`;
|
|
20
|
-
return Platform.MapUtilities.getWithDefault(scriptById, key, () => ({ isolate, scriptId, frame: '', ts:
|
|
20
|
+
return Platform.MapUtilities.getWithDefault(scriptById, key, () => ({ isolate, scriptId, frame: '', ts: event.ts }));
|
|
21
21
|
};
|
|
22
22
|
if (Types.Events.isRundownScriptCompiled(event) && event.args.data) {
|
|
23
23
|
const { isolate, scriptId, frame } = event.args.data;
|
|
@@ -30,6 +30,7 @@ export function handleEvent(event) {
|
|
|
30
30
|
const { isolate, scriptId, url, sourceUrl, sourceMapUrl, sourceMapUrlElided } = event.args.data;
|
|
31
31
|
const script = getOrMakeScript(isolate, scriptId);
|
|
32
32
|
script.url = url;
|
|
33
|
+
script.ts = event.ts;
|
|
33
34
|
if (sourceUrl) {
|
|
34
35
|
script.sourceUrl = sourceUrl;
|
|
35
36
|
}
|
|
@@ -48,7 +48,8 @@ export function generateInsight(data, context) {
|
|
|
48
48
|
if (script.url?.startsWith('chrome-extension://')) {
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
|
-
return Helpers.Timing.timestampIsInBounds(context.bounds, script.ts)
|
|
51
|
+
return Helpers.Timing.timestampIsInBounds(context.bounds, script.ts) ||
|
|
52
|
+
(script.request && Helpers.Timing.eventIsInBounds(script.request, context.bounds));
|
|
52
53
|
});
|
|
53
54
|
const legacyJavaScriptResults = new Map();
|
|
54
55
|
const wastedBytesByRequestId = new Map();
|
package/build/src/McpContext.js
CHANGED
|
@@ -223,7 +223,9 @@ export class McpContext {
|
|
|
223
223
|
*/
|
|
224
224
|
async createTextSnapshot() {
|
|
225
225
|
const page = this.getSelectedPage();
|
|
226
|
-
const rootNode = await page.accessibility.snapshot(
|
|
226
|
+
const rootNode = await page.accessibility.snapshot({
|
|
227
|
+
includeIframes: true,
|
|
228
|
+
});
|
|
227
229
|
if (!rootNode) {
|
|
228
230
|
return;
|
|
229
231
|
}
|
|
@@ -256,8 +258,13 @@ export class McpContext {
|
|
|
256
258
|
async saveTemporaryFile(data, mimeType) {
|
|
257
259
|
try {
|
|
258
260
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
+
const ext = mimeType === 'image/png'
|
|
262
|
+
? 'png'
|
|
263
|
+
: mimeType === 'image/jpeg'
|
|
264
|
+
? 'jpg'
|
|
265
|
+
: 'webp';
|
|
266
|
+
const filename = path.join(dir, `screenshot.${ext}`);
|
|
267
|
+
await fs.writeFile(filename, data);
|
|
261
268
|
return { filename };
|
|
262
269
|
}
|
|
263
270
|
catch (err) {
|
package/build/src/McpResponse.js
CHANGED
|
@@ -96,7 +96,7 @@ export class McpResponse {
|
|
|
96
96
|
if (networkConditions) {
|
|
97
97
|
response.push(`## Network emulation`);
|
|
98
98
|
response.push(`Emulating: ${networkConditions}`);
|
|
99
|
-
response.push(`
|
|
99
|
+
response.push(`Default navigation timeout set to ${context.getNavigationTimeout()} ms`);
|
|
100
100
|
}
|
|
101
101
|
const cpuThrottlingRate = context.getCpuThrottlingRate();
|
|
102
102
|
if (cpuThrottlingRate > 1) {
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
export class PageCollector {
|
|
7
7
|
#browser;
|
|
8
8
|
#initializer;
|
|
9
|
+
/**
|
|
10
|
+
* The Array in this map should only be set once
|
|
11
|
+
* As we use the reference to it.
|
|
12
|
+
* Use methods that manipulate the array in place.
|
|
13
|
+
*/
|
|
9
14
|
storage = new WeakMap();
|
|
10
15
|
constructor(browser, initializer) {
|
|
11
16
|
this.#browser = browser;
|
|
@@ -31,6 +36,8 @@ export class PageCollector {
|
|
|
31
36
|
if (this.storage.has(page)) {
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
39
|
+
const stored = [];
|
|
40
|
+
this.storage.set(page, stored);
|
|
34
41
|
page.on('framenavigated', frame => {
|
|
35
42
|
// Only reset the storage on main frame navigation
|
|
36
43
|
if (frame !== page.mainFrame()) {
|
|
@@ -39,15 +46,15 @@ export class PageCollector {
|
|
|
39
46
|
this.cleanup(page);
|
|
40
47
|
});
|
|
41
48
|
this.#initializer(page, value => {
|
|
42
|
-
const stored = this.storage.get(page) ?? [];
|
|
43
49
|
stored.push(value);
|
|
44
|
-
this.storage.set(page, stored);
|
|
45
50
|
});
|
|
46
51
|
}
|
|
47
52
|
cleanup(page) {
|
|
48
|
-
const collection = this.storage.get(page)
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const collection = this.storage.get(page);
|
|
54
|
+
if (collection) {
|
|
55
|
+
// Keep the reference alive
|
|
56
|
+
collection.length = 0;
|
|
57
|
+
}
|
|
51
58
|
}
|
|
52
59
|
getData(page) {
|
|
53
60
|
return this.storage.get(page) ?? [];
|
|
@@ -56,6 +63,9 @@ export class PageCollector {
|
|
|
56
63
|
export class NetworkCollector extends PageCollector {
|
|
57
64
|
cleanup(page) {
|
|
58
65
|
const requests = this.storage.get(page) ?? [];
|
|
66
|
+
if (!requests) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
59
69
|
const lastRequestIdx = requests.findLastIndex(request => {
|
|
60
70
|
return request.frame() === page.mainFrame()
|
|
61
71
|
? request.isNavigationRequest()
|
|
@@ -63,6 +73,7 @@ export class NetworkCollector extends PageCollector {
|
|
|
63
73
|
});
|
|
64
74
|
// Keep all requests since the last navigation request including that
|
|
65
75
|
// navigation request itself.
|
|
66
|
-
|
|
76
|
+
// Keep the reference
|
|
77
|
+
requests.splice(0, Math.max(lastRequestIdx, 0));
|
|
67
78
|
}
|
|
68
79
|
}
|
package/build/src/browser.js
CHANGED
|
@@ -30,7 +30,7 @@ const connectOptions = {
|
|
|
30
30
|
// We do not expect any single CDP command to take more than 10sec.
|
|
31
31
|
protocolTimeout: 10_000,
|
|
32
32
|
};
|
|
33
|
-
async function ensureBrowserConnected(browserURL) {
|
|
33
|
+
export async function ensureBrowserConnected(browserURL) {
|
|
34
34
|
if (browser?.connected) {
|
|
35
35
|
return browser;
|
|
36
36
|
}
|
|
@@ -53,7 +53,10 @@ export async function launch(options) {
|
|
|
53
53
|
recursive: true,
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
const args = [
|
|
56
|
+
const args = [
|
|
57
|
+
...(options.args ?? []),
|
|
58
|
+
'--hide-crash-restore-bubble',
|
|
59
|
+
];
|
|
57
60
|
if (customDevTools) {
|
|
58
61
|
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
|
|
59
62
|
}
|
|
@@ -74,6 +77,7 @@ export async function launch(options) {
|
|
|
74
77
|
pipe: true,
|
|
75
78
|
headless,
|
|
76
79
|
args,
|
|
80
|
+
acceptInsecureCerts: options.acceptInsecureCerts,
|
|
77
81
|
});
|
|
78
82
|
if (options.logFile) {
|
|
79
83
|
// FIXME: we are probably subscribing too late to catch startup logs. We
|
|
@@ -81,6 +85,14 @@ export async function launch(options) {
|
|
|
81
85
|
browser.process()?.stderr?.pipe(options.logFile);
|
|
82
86
|
browser.process()?.stdout?.pipe(options.logFile);
|
|
83
87
|
}
|
|
88
|
+
if (options.viewport) {
|
|
89
|
+
const [page] = await browser.pages();
|
|
90
|
+
// @ts-expect-error internal API for now.
|
|
91
|
+
await page?.resize({
|
|
92
|
+
contentWidth: options.viewport.width,
|
|
93
|
+
contentHeight: options.viewport.height,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
84
96
|
return browser;
|
|
85
97
|
}
|
|
86
98
|
catch (error) {
|
|
@@ -95,16 +107,10 @@ export async function launch(options) {
|
|
|
95
107
|
throw error;
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
|
-
async function ensureBrowserLaunched(options) {
|
|
110
|
+
export async function ensureBrowserLaunched(options) {
|
|
99
111
|
if (browser?.connected) {
|
|
100
112
|
return browser;
|
|
101
113
|
}
|
|
102
114
|
browser = await launch(options);
|
|
103
115
|
return browser;
|
|
104
116
|
}
|
|
105
|
-
export async function resolveBrowser(options) {
|
|
106
|
-
const browser = options.browserUrl
|
|
107
|
-
? await ensureBrowserConnected(options.browserUrl)
|
|
108
|
-
: await ensureBrowserLaunched(options);
|
|
109
|
-
return browser;
|
|
110
|
-
}
|
package/build/src/cli.js
CHANGED
|
@@ -48,6 +48,31 @@ export const cliOptions = {
|
|
|
48
48
|
type: 'string',
|
|
49
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.',
|
|
50
50
|
},
|
|
51
|
+
viewport: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
describe: 'Initial viewport size for the Chromee instances started by the server. For example, `1280x720`',
|
|
54
|
+
coerce: (arg) => {
|
|
55
|
+
if (arg === undefined) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const [width, height] = arg.split('x').map(Number);
|
|
59
|
+
if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) {
|
|
60
|
+
throw new Error('Invalid viewport. Expected format is `1280x720`.');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
width,
|
|
64
|
+
height,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
proxyServer: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: `Proxy server configuration for Chrome passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.`,
|
|
71
|
+
},
|
|
72
|
+
acceptInsecureCerts: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
|
|
75
|
+
},
|
|
51
76
|
};
|
|
52
77
|
export function parseArguments(version, argv = process.argv) {
|
|
53
78
|
const yargsInstance = yargs(hideBin(argv))
|
|
@@ -72,6 +97,10 @@ export function parseArguments(version, argv = process.argv) {
|
|
|
72
97
|
['$0 --channel stable', 'Use stable Chrome installed on this system'],
|
|
73
98
|
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
|
|
74
99
|
['$0 --help', 'Print CLI options'],
|
|
100
|
+
[
|
|
101
|
+
'$0 --viewport 1280x720',
|
|
102
|
+
'Launch Chrome with the initial viewport size of 1280x720px',
|
|
103
|
+
],
|
|
75
104
|
]);
|
|
76
105
|
return yargsInstance
|
|
77
106
|
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
package/build/src/index.js
CHANGED
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { version } from 'node:process';
|
|
8
8
|
const [major, minor] = version.substring(1).split('.').map(Number);
|
|
9
|
+
if (major === 20 && minor < 19) {
|
|
10
|
+
console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
9
13
|
if (major === 22 && minor < 12) {
|
|
10
14
|
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
15
|
process.exit(1);
|
|
12
16
|
}
|
|
13
17
|
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.`);
|
|
18
|
+
console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 20.19.0 LTS or a newer LTS.`);
|
|
15
19
|
process.exit(1);
|
|
16
20
|
}
|
|
17
21
|
await import('./main.js');
|
package/build/src/logger.js
CHANGED
|
@@ -13,12 +13,12 @@ const namespacesToEnable = [
|
|
|
13
13
|
export function saveLogsToFile(fileName) {
|
|
14
14
|
// Enable overrides everything so we need to add them
|
|
15
15
|
debug.enable(namespacesToEnable.join(','));
|
|
16
|
-
const logFile = fs.createWriteStream(fileName, { flags: 'a' });
|
|
16
|
+
const logFile = fs.createWriteStream(fileName, { flags: 'a+' });
|
|
17
17
|
debug.log = function (...chunks) {
|
|
18
18
|
logFile.write(`${chunks.join(' ')}\n`);
|
|
19
19
|
};
|
|
20
20
|
logFile.on('error', function (error) {
|
|
21
|
-
console.
|
|
21
|
+
console.error(`Error when opening/writing to log file: ${error.message}`);
|
|
22
22
|
logFile.end();
|
|
23
23
|
process.exit(1);
|
|
24
24
|
});
|
package/build/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
12
|
import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
import {
|
|
13
|
+
import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
|
|
14
14
|
import { parseArguments } from './cli.js';
|
|
15
15
|
import { logger, saveLogsToFile } from './logger.js';
|
|
16
16
|
import { McpContext } from './McpContext.js';
|
|
@@ -54,15 +54,23 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
|
|
|
54
54
|
});
|
|
55
55
|
let context;
|
|
56
56
|
async function getContext() {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
const extraArgs = [];
|
|
58
|
+
if (args.proxyServer) {
|
|
59
|
+
extraArgs.push(`--proxy-server=${args.proxyServer}`);
|
|
60
|
+
}
|
|
61
|
+
const browser = args.browserUrl
|
|
62
|
+
? await ensureBrowserConnected(args.browserUrl)
|
|
63
|
+
: await ensureBrowserLaunched({
|
|
64
|
+
headless: args.headless,
|
|
65
|
+
executablePath: args.executablePath,
|
|
66
|
+
customDevTools: args.customDevtools,
|
|
67
|
+
channel: args.channel,
|
|
68
|
+
isolated: args.isolated,
|
|
69
|
+
logFile,
|
|
70
|
+
viewport: args.viewport,
|
|
71
|
+
args: extraArgs,
|
|
72
|
+
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
73
|
+
});
|
|
66
74
|
if (context?.browser !== browser) {
|
|
67
75
|
context = await McpContext.from(browser, logger);
|
|
68
76
|
}
|
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import z from 'zod';
|
|
6
7
|
export function defineTool(definition) {
|
|
7
8
|
return definition;
|
|
8
9
|
}
|
|
9
10
|
export const CLOSE_PAGE_ERROR = 'The last open page cannot be closed. It is fine to keep it open.';
|
|
11
|
+
export const timeoutSchema = {
|
|
12
|
+
timeout: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe(`Maximum wait time in milliseconds. If set to 0, the default timeout will be used.`)
|
|
17
|
+
.transform(value => {
|
|
18
|
+
return value && value <= 0 ? undefined : value;
|
|
19
|
+
}),
|
|
20
|
+
};
|
package/build/src/tools/input.js
CHANGED
|
@@ -180,7 +180,7 @@ export const uploadFile = defineTool({
|
|
|
180
180
|
// a type=file element. In this case, we want to default to
|
|
181
181
|
// Page.waitForFileChooser() and upload the file this way.
|
|
182
182
|
try {
|
|
183
|
-
const page =
|
|
183
|
+
const page = context.getSelectedPage();
|
|
184
184
|
const [fileChooser] = await Promise.all([
|
|
185
185
|
page.waitForFileChooser({ timeout: 3000 }),
|
|
186
186
|
handle.asLocator().click(),
|
package/build/src/tools/pages.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
|
-
import { CLOSE_PAGE_ERROR, defineTool } from './ToolDefinition.js';
|
|
9
|
+
import { CLOSE_PAGE_ERROR, defineTool, timeoutSchema } from './ToolDefinition.js';
|
|
10
10
|
export const listPages = defineTool({
|
|
11
11
|
name: 'list_pages',
|
|
12
12
|
description: `Get a list of pages open in the browser.`,
|
|
@@ -74,11 +74,14 @@ export const newPage = defineTool({
|
|
|
74
74
|
},
|
|
75
75
|
schema: {
|
|
76
76
|
url: z.string().describe('URL to load in a new page.'),
|
|
77
|
+
...timeoutSchema,
|
|
77
78
|
},
|
|
78
79
|
handler: async (request, response, context) => {
|
|
79
80
|
const page = await context.newPage();
|
|
80
81
|
await context.waitForEventsAfterAction(async () => {
|
|
81
|
-
await page.goto(request.params.url
|
|
82
|
+
await page.goto(request.params.url, {
|
|
83
|
+
timeout: request.params.timeout,
|
|
84
|
+
});
|
|
82
85
|
});
|
|
83
86
|
response.setIncludePages(true);
|
|
84
87
|
},
|
|
@@ -92,11 +95,14 @@ export const navigatePage = defineTool({
|
|
|
92
95
|
},
|
|
93
96
|
schema: {
|
|
94
97
|
url: z.string().describe('URL to navigate the page to'),
|
|
98
|
+
...timeoutSchema,
|
|
95
99
|
},
|
|
96
100
|
handler: async (request, response, context) => {
|
|
97
101
|
const page = context.getSelectedPage();
|
|
98
102
|
await context.waitForEventsAfterAction(async () => {
|
|
99
|
-
await page.goto(request.params.url
|
|
103
|
+
await page.goto(request.params.url, {
|
|
104
|
+
timeout: request.params.timeout,
|
|
105
|
+
});
|
|
100
106
|
});
|
|
101
107
|
response.setIncludePages(true);
|
|
102
108
|
},
|
|
@@ -112,15 +118,19 @@ export const navigatePageHistory = defineTool({
|
|
|
112
118
|
navigate: z
|
|
113
119
|
.enum(['back', 'forward'])
|
|
114
120
|
.describe('Whether to navigate back or navigate forward in the selected pages history'),
|
|
121
|
+
...timeoutSchema,
|
|
115
122
|
},
|
|
116
123
|
handler: async (request, response, context) => {
|
|
117
124
|
const page = context.getSelectedPage();
|
|
125
|
+
const options = {
|
|
126
|
+
timeout: request.params.timeout,
|
|
127
|
+
};
|
|
118
128
|
try {
|
|
119
129
|
if (request.params.navigate === 'back') {
|
|
120
|
-
await page.goBack();
|
|
130
|
+
await page.goBack(options);
|
|
121
131
|
}
|
|
122
132
|
else {
|
|
123
|
-
await page.goForward();
|
|
133
|
+
await page.goForward(options);
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
catch {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { writeFile } from 'node:fs/promises';
|
|
6
7
|
import z from 'zod';
|
|
7
8
|
import { ToolCategories } from './categories.js';
|
|
8
9
|
import { defineTool } from './ToolDefinition.js';
|
|
@@ -15,7 +16,7 @@ export const screenshot = defineTool({
|
|
|
15
16
|
},
|
|
16
17
|
schema: {
|
|
17
18
|
format: z
|
|
18
|
-
.enum(['png', 'jpeg'])
|
|
19
|
+
.enum(['png', 'jpeg', 'webp'])
|
|
19
20
|
.default('png')
|
|
20
21
|
.describe('Type of format to save the screenshot as. Default is "png"'),
|
|
21
22
|
quality: z
|
|
@@ -23,7 +24,7 @@ export const screenshot = defineTool({
|
|
|
23
24
|
.min(0)
|
|
24
25
|
.max(100)
|
|
25
26
|
.optional()
|
|
26
|
-
.describe('Compression quality for JPEG
|
|
27
|
+
.describe('Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
|
|
27
28
|
uid: z
|
|
28
29
|
.string()
|
|
29
30
|
.optional()
|
|
@@ -32,6 +33,10 @@ export const screenshot = defineTool({
|
|
|
32
33
|
.boolean()
|
|
33
34
|
.optional()
|
|
34
35
|
.describe('If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.'),
|
|
36
|
+
filePath: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.'),
|
|
35
40
|
},
|
|
36
41
|
handler: async (request, response, context) => {
|
|
37
42
|
if (request.params.uid && request.params.fullPage) {
|
|
@@ -59,7 +64,11 @@ export const screenshot = defineTool({
|
|
|
59
64
|
else {
|
|
60
65
|
response.appendResponseLine("Took a screenshot of the current page's viewport.");
|
|
61
66
|
}
|
|
62
|
-
if (
|
|
67
|
+
if (request.params.filePath) {
|
|
68
|
+
await writeFile(request.params.filePath, screenshot);
|
|
69
|
+
response.appendResponseLine(`Saved screenshot to ${request.params.filePath}.`);
|
|
70
|
+
}
|
|
71
|
+
else if (screenshot.length >= 2_000_000) {
|
|
63
72
|
const { filename } = await context.saveTemporaryFile(screenshot, `image/${request.params.format}`);
|
|
64
73
|
response.appendResponseLine(`Saved screenshot to ${filename}.`);
|
|
65
74
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Locator } from 'puppeteer-core';
|
|
7
7
|
import z from 'zod';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
|
-
import { defineTool } from './ToolDefinition.js';
|
|
9
|
+
import { defineTool, timeoutSchema } from './ToolDefinition.js';
|
|
10
10
|
export const takeSnapshot = defineTool({
|
|
11
11
|
name: 'take_snapshot',
|
|
12
12
|
description: `Take a text snapshot of the currently selected page. The snapshot lists page elements along with a unique
|
|
@@ -29,13 +29,19 @@ export const waitFor = defineTool({
|
|
|
29
29
|
},
|
|
30
30
|
schema: {
|
|
31
31
|
text: z.string().describe('Text to appear on the page'),
|
|
32
|
+
...timeoutSchema,
|
|
32
33
|
},
|
|
33
34
|
handler: async (request, response, context) => {
|
|
34
35
|
const page = context.getSelectedPage();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const frames = page.frames();
|
|
37
|
+
const locator = Locator.race(frames.flatMap(frame => [
|
|
38
|
+
frame.locator(`aria/${request.params.text}`),
|
|
39
|
+
frame.locator(`text/${request.params.text}`),
|
|
40
|
+
]));
|
|
41
|
+
if (request.params.timeout) {
|
|
42
|
+
locator.setTimeout(request.params.timeout);
|
|
43
|
+
}
|
|
44
|
+
await locator.wait();
|
|
39
45
|
response.appendResponseLine(`Element with text "${request.params.text}" found.`);
|
|
40
46
|
response.setIncludeSnapshot(true);
|
|
41
47
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP server for Chrome DevTools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@types/yargs": "^17.0.33",
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
|
55
55
|
"@typescript-eslint/parser": "^8.43.0",
|
|
56
|
-
"chrome-devtools-frontend": "1.0.
|
|
56
|
+
"chrome-devtools-frontend": "1.0.1521880",
|
|
57
57
|
"eslint": "^9.35.0",
|
|
58
58
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
59
59
|
"eslint-plugin-import": "^2.32.0",
|
|
@@ -65,6 +65,6 @@
|
|
|
65
65
|
"typescript-eslint": "^8.43.0"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
|
-
"node": "
|
|
68
|
+
"node": "^20.19.0 || ^22.12.0 || >=23"
|
|
69
69
|
}
|
|
70
70
|
}
|