chrome-devtools-mcp 0.5.1 → 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.
Files changed (25) hide show
  1. package/README.md +50 -2
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHost.js +3 -0
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +6 -0
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSRule.js +4 -0
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSStartingStyle.js +21 -0
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +19 -1
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/sdk.js +2 -1
  9. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +3 -2
  10. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScriptsHandler.js +2 -1
  11. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LegacyJavaScript.js +2 -1
  12. package/build/src/McpContext.js +10 -3
  13. package/build/src/McpResponse.js +1 -1
  14. package/build/src/PageCollector.js +17 -6
  15. package/build/src/browser.js +15 -9
  16. package/build/src/cli.js +29 -0
  17. package/build/src/logger.js +2 -2
  18. package/build/src/main.js +18 -10
  19. package/build/src/tools/ToolDefinition.js +11 -0
  20. package/build/src/tools/input.js +1 -1
  21. package/build/src/tools/network.js +0 -1
  22. package/build/src/tools/pages.js +15 -5
  23. package/build/src/tools/screenshot.js +12 -3
  24. package/build/src/tools/snapshot.js +11 -5
  25. package/package.json +2 -2
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=eyJjb21tYW5kIjoibnB4IGNocm9tZS1kZXZ0b29scy1tY3BAbGF0ZXN0In0%3D)
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:
@@ -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;
@@ -164,7 +164,11 @@ class RehydratingSessionBase {
164
164
  this.connection = connection;
165
165
  }
166
166
  sendMessageToFrontend(payload) {
167
- this.connection?.postToFrontend(payload);
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, };
@@ -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: 0 }));
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();
@@ -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 filename = path.join(dir, mimeType == 'image/png' ? `screenshot.png` : 'screenshot.jpg');
260
- await fs.writeFile(path.join(dir, `screenshot.png`), data);
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) {
@@ -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(`Navigation timeout set to ${context.getNavigationTimeout()} ms`);
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
- // Keep the reference alive
50
- collection.length = 0;
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
- this.storage.set(page, requests.slice(Math.max(lastRequestIdx, 0)));
76
+ // Keep the reference
77
+ requests.splice(0, Math.max(lastRequestIdx, 0));
67
78
  }
68
79
  }
@@ -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 = ['--hide-crash-restore-bubble'];
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()))
@@ -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.log(`Error when opening/writing to log file: ${error.message}`);
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 { resolveBrowser } from './browser.js';
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 browser = await resolveBrowser({
58
- browserUrl: args.browserUrl,
59
- headless: args.headless,
60
- executablePath: args.executablePath,
61
- customDevTools: args.customDevtools,
62
- channel: args.channel,
63
- isolated: args.isolated,
64
- logFile,
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
+ };
@@ -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 = await context.getSelectedPage();
183
+ const page = context.getSelectedPage();
184
184
  const [fileChooser] = await Promise.all([
185
185
  page.waitForFileChooser({ timeout: 3000 }),
186
186
  handle.asLocator().click(),
@@ -72,6 +72,5 @@ export const getNetworkRequest = defineTool({
72
72
  },
73
73
  handler: async (request, response, _context) => {
74
74
  response.attachNetworkRequest(request.params.url);
75
- response.setIncludeNetworkRequests(true);
76
75
  },
77
76
  });
@@ -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 format (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
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 (screenshot.length >= 2_000_000) {
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
- await Locator.race([
36
- page.locator(`aria/${request.params.text}`),
37
- page.locator(`text/${request.params.text}`),
38
- ]).wait();
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.5.1",
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.1520535",
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",