chrome-devtools-mcp 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,7 +111,7 @@ Start the dialog to add a new MCP server by running:
111
111
  /mcp add
112
112
  ```
113
113
 
114
- Configure the following fields and press `CTR-S` to save the configuration:
114
+ Configure the following fields and press `CTRL+S` to save the configuration:
115
115
 
116
116
  - **Server name:** `chrome-devtools`
117
117
  - **Server Type:** `[1] Local`
@@ -328,6 +328,69 @@ all instances of `chrome-devtools-mcp`. Set the `isolated` option to `true`
328
328
  to use a temporary user data dir instead which will be cleared automatically after
329
329
  the browser is closed.
330
330
 
331
+ ### Connecting to a running Chrome instance
332
+
333
+ You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you want to use your existing Chrome profile or if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance.
334
+
335
+ Here is a step-by-step guide on how to connect to a running Chrome Stable instance:
336
+
337
+ **Step 1: Configure the MCP client**
338
+
339
+ Add the `--browser-url` option to your MCP client configuration. The value of this option should be the URL of the running Chrome instance. `http://localhost:9222` is a common default.
340
+
341
+ ```json
342
+ {
343
+ "mcpServers": {
344
+ "chrome-devtools": {
345
+ "command": "npx",
346
+ "args": [
347
+ "chrome-devtools-mcp@latest",
348
+ "--browser-url=http://localhost:9222"
349
+ ]
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ **Step 2: Start the Chrome browser**
356
+
357
+ > [!WARNING]
358
+ > Enabling the remote debugging port opens up a debugging port on the running browser instance. Any application on your machine can connect to this port and control the browser. Make sure that you are not browsing any sensitive websites while the debugging port is open.
359
+
360
+ Start the Chrome browser with the remote debugging port enabled. Make sure to close any running Chrome instances before starting a new one with the debugging port enabled. The port number you choose must be the same as the one you specified in the `--browser-url` option in your MCP client configuration.
361
+
362
+ For security reasons, [Chrome requires you to use a non-default user data directory](https://developer.chrome.com/blog/remote-debugging-port) when enabling the remote debugging port. You can specify a custom directory using the `--user-data-dir` flag. This ensures that your regular browsing profile and data are not exposed to the debugging session.
363
+
364
+ **macOS**
365
+
366
+ ```bash
367
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable
368
+ ```
369
+
370
+ **Linux**
371
+
372
+ ```bash
373
+ /usr/bin/google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile-stable
374
+ ```
375
+
376
+ **Windows**
377
+
378
+ ```bash
379
+ "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-profile-stable"
380
+ ```
381
+
382
+ **Step 3: Test your setup**
383
+
384
+ After configuring the MCP client and starting the Chrome browser, you can test your setup by running a simple prompt in your MCP client:
385
+
386
+ ```
387
+ Check the performance of https://developers.chrome.com
388
+ ```
389
+
390
+ Your MCP client should connect to the running Chrome instance and receive a performance report.
391
+
392
+ For more details on remote debugging, see the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/remote-debugging/).
393
+
331
394
  ## Known limitations
332
395
 
333
396
  ### Operating system sandboxes
@@ -336,5 +399,5 @@ Some MCP clients allow sandboxing the MCP server using macOS Seatbelt or Linux
336
399
  containers. If sandboxes are enabled, `chrome-devtools-mcp` is not able to start
337
400
  Chrome that requires permissions to create its own sandboxes. As a workaround,
338
401
  either disable sandboxing for `chrome-devtools-mcp` in your MCP client or use
339
- `--connect-url` to connect to a Chrome instance that you start manually outside
402
+ `--browser-url` to connect to a Chrome instance that you start manually outside
340
403
  of the MCP client sandbox.
@@ -1,12 +1,12 @@
1
1
  import { formatConsoleEvent } from './formatters/consoleFormatter.js';
2
- import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
2
+ import { getFormattedHeaderValue, getFormattedResponseBody, getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
3
3
  import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
4
4
  import { handleDialog } from './tools/pages.js';
5
5
  import { paginate } from './utils/pagination.js';
6
6
  export class McpResponse {
7
7
  #includePages = false;
8
8
  #includeSnapshot = false;
9
- #attachedNetworkRequestUrl;
9
+ #attachedNetworkRequestData;
10
10
  #includeConsoleData = false;
11
11
  #textResponseLines = [];
12
12
  #formattedConsoleData;
@@ -38,7 +38,9 @@ export class McpResponse {
38
38
  this.#includeConsoleData = value;
39
39
  }
40
40
  attachNetworkRequest(url) {
41
- this.#attachedNetworkRequestUrl = url;
41
+ this.#attachedNetworkRequestData = {
42
+ networkRequestUrl: url,
43
+ };
42
44
  }
43
45
  get includePages() {
44
46
  return this.#includePages;
@@ -50,7 +52,7 @@ export class McpResponse {
50
52
  return this.#includeConsoleData;
51
53
  }
52
54
  get attachedNetworkRequestUrl() {
53
- return this.#attachedNetworkRequestUrl;
55
+ return this.#attachedNetworkRequestData?.networkRequestUrl;
54
56
  }
55
57
  get networkRequestsPageIdx() {
56
58
  return this.#networkRequestsOptions?.pagination?.pageIdx;
@@ -78,6 +80,16 @@ export class McpResponse {
78
80
  await context.createTextSnapshot();
79
81
  }
80
82
  let formattedConsoleMessages;
83
+ if (this.#attachedNetworkRequestData?.networkRequestUrl) {
84
+ const request = context.getNetworkRequestByUrl(this.#attachedNetworkRequestData.networkRequestUrl);
85
+ this.#attachedNetworkRequestData.requestBody =
86
+ await getFormattedRequestBody(request);
87
+ const response = request.response();
88
+ if (response) {
89
+ this.#attachedNetworkRequestData.responseBody =
90
+ await getFormattedResponseBody(response);
91
+ }
92
+ }
81
93
  if (this.#includeConsoleData) {
82
94
  const consoleMessages = context.getConsoleData();
83
95
  if (consoleMessages) {
@@ -193,7 +205,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
193
205
  }
194
206
  #getIncludeNetworkRequestsData(context) {
195
207
  const response = [];
196
- const url = this.#attachedNetworkRequestUrl;
208
+ const url = this.#attachedNetworkRequestData?.networkRequestUrl;
197
209
  if (!url) {
198
210
  return response;
199
211
  }
@@ -204,6 +216,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
204
216
  for (const line of getFormattedHeaderValue(httpRequest.headers())) {
205
217
  response.push(line);
206
218
  }
219
+ if (this.#attachedNetworkRequestData?.requestBody) {
220
+ response.push(`### Request Body`);
221
+ response.push(this.#attachedNetworkRequestData.requestBody);
222
+ }
207
223
  const httpResponse = httpRequest.response();
208
224
  if (httpResponse) {
209
225
  response.push(`### Response Headers`);
@@ -211,6 +227,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
211
227
  response.push(line);
212
228
  }
213
229
  }
230
+ if (this.#attachedNetworkRequestData?.responseBody) {
231
+ response.push(`### Response Body`);
232
+ response.push(this.#attachedNetworkRequestData.responseBody);
233
+ }
214
234
  const httpFailure = httpRequest.failure();
215
235
  if (httpFailure) {
216
236
  response.push(`### Request failed with`);
@@ -8,36 +8,36 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import puppeteer from 'puppeteer-core';
10
10
  let browser;
11
- const ignoredPrefixes = new Set([
12
- 'chrome://',
13
- 'chrome-extension://',
14
- 'chrome-untrusted://',
15
- 'devtools://',
16
- ]);
17
- function targetFilter(target) {
18
- if (target.url() === 'chrome://newtab/') {
19
- return true;
11
+ function makeTargetFilter(devtools) {
12
+ const ignoredPrefixes = new Set([
13
+ 'chrome://',
14
+ 'chrome-extension://',
15
+ 'chrome-untrusted://',
16
+ ]);
17
+ if (!devtools) {
18
+ ignoredPrefixes.add('devtools://');
20
19
  }
21
- for (const prefix of ignoredPrefixes) {
22
- if (target.url().startsWith(prefix)) {
23
- return false;
20
+ return function targetFilter(target) {
21
+ if (target.url() === 'chrome://newtab/') {
22
+ return true;
24
23
  }
25
- }
26
- return true;
24
+ for (const prefix of ignoredPrefixes) {
25
+ if (target.url().startsWith(prefix)) {
26
+ return false;
27
+ }
28
+ }
29
+ return true;
30
+ };
27
31
  }
28
- const connectOptions = {
29
- targetFilter,
30
- // We do not expect any single CDP command to take more than 10sec.
31
- protocolTimeout: 10_000,
32
- };
33
- export async function ensureBrowserConnected(browserURL) {
32
+ export async function ensureBrowserConnected(options) {
34
33
  if (browser?.connected) {
35
34
  return browser;
36
35
  }
37
36
  browser = await puppeteer.connect({
38
- ...connectOptions,
39
- browserURL,
37
+ targetFilter: makeTargetFilter(options.devtools),
38
+ browserURL: options.browserURL,
40
39
  defaultViewport: null,
40
+ handleDevToolsAsPage: options.devtools,
41
41
  });
42
42
  return browser;
43
43
  }
@@ -64,6 +64,9 @@ export async function launch(options) {
64
64
  args.push('--screen-info={3840x2160}');
65
65
  }
66
66
  let puppeteerChannel;
67
+ if (options.devtools) {
68
+ args.push('--auto-open-devtools-for-tabs');
69
+ }
67
70
  if (!executablePath) {
68
71
  puppeteerChannel =
69
72
  channel && channel !== 'stable'
@@ -72,8 +75,8 @@ export async function launch(options) {
72
75
  }
73
76
  try {
74
77
  const browser = await puppeteer.launch({
75
- ...connectOptions,
76
78
  channel: puppeteerChannel,
79
+ targetFilter: makeTargetFilter(options.devtools),
77
80
  executablePath,
78
81
  defaultViewport: null,
79
82
  userDataDir,
@@ -81,6 +84,7 @@ export async function launch(options) {
81
84
  headless,
82
85
  args,
83
86
  acceptInsecureCerts: options.acceptInsecureCerts,
87
+ handleDevToolsAsPage: options.devtools,
84
88
  });
85
89
  if (options.logFile) {
86
90
  // FIXME: we are probably subscribing too late to catch startup logs. We
package/build/src/cli.js CHANGED
@@ -81,6 +81,11 @@ export const cliOptions = {
81
81
  type: 'boolean',
82
82
  description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
83
83
  },
84
+ experimentalDevtools: {
85
+ type: 'boolean',
86
+ describe: 'Whether to enable automation over DevTools targets',
87
+ hidden: true,
88
+ },
84
89
  };
85
90
  export function parseArguments(version, argv = process.argv) {
86
91
  const yargsInstance = yargs(hideBin(argv))
@@ -3,6 +3,8 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import { isUtf8 } from 'node:buffer';
7
+ const BODY_CONTEXT_SIZE_LIMIT = 10000;
6
8
  export function getShortDescriptionForRequest(request) {
7
9
  return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`;
8
10
  }
@@ -32,3 +34,45 @@ export function getFormattedHeaderValue(headers) {
32
34
  }
33
35
  return response;
34
36
  }
37
+ export async function getFormattedResponseBody(httpResponse, sizeLimit = BODY_CONTEXT_SIZE_LIMIT) {
38
+ try {
39
+ const responseBuffer = await httpResponse.buffer();
40
+ if (isUtf8(responseBuffer)) {
41
+ const responseAsTest = responseBuffer.toString('utf-8');
42
+ if (responseAsTest.length === 0) {
43
+ return `<empty response>`;
44
+ }
45
+ return `${getSizeLimitedString(responseAsTest, sizeLimit)}`;
46
+ }
47
+ return `<binary data>`;
48
+ }
49
+ catch {
50
+ // buffer() call might fail with CDP exception, in this case we don't print anything in the context
51
+ return;
52
+ }
53
+ }
54
+ export async function getFormattedRequestBody(httpRequest, sizeLimit = BODY_CONTEXT_SIZE_LIMIT) {
55
+ if (httpRequest.hasPostData()) {
56
+ const data = httpRequest.postData();
57
+ if (data) {
58
+ return `${getSizeLimitedString(data, sizeLimit)}`;
59
+ }
60
+ try {
61
+ const fetchData = await httpRequest.fetchPostData();
62
+ if (fetchData) {
63
+ return `${getSizeLimitedString(fetchData, sizeLimit)}`;
64
+ }
65
+ }
66
+ catch {
67
+ // fetchPostData() call might fail with CDP exception, in this case we don't print anything in the context
68
+ return;
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ function getSizeLimitedString(text, sizeLimit) {
74
+ if (text.length > sizeLimit) {
75
+ return `${text.substring(0, sizeLimit) + '... <truncated>'}`;
76
+ }
77
+ return `${text}`;
78
+ }
package/build/src/main.js CHANGED
@@ -58,8 +58,12 @@ async function getContext() {
58
58
  if (args.proxyServer) {
59
59
  extraArgs.push(`--proxy-server=${args.proxyServer}`);
60
60
  }
61
+ const devtools = args.experimentalDevtools ?? false;
61
62
  const browser = args.browserUrl
62
- ? await ensureBrowserConnected(args.browserUrl)
63
+ ? await ensureBrowserConnected({
64
+ browserURL: args.browserUrl,
65
+ devtools,
66
+ })
63
67
  : await ensureBrowserLaunched({
64
68
  headless: args.headless,
65
69
  executablePath: args.executablePath,
@@ -70,6 +74,7 @@ async function getContext() {
70
74
  viewport: args.viewport,
71
75
  args: extraArgs,
72
76
  acceptInsecureCerts: args.acceptInsecureCerts,
77
+ devtools,
73
78
  });
74
79
  if (context?.browser !== browser) {
75
80
  context = await McpContext.from(browser, logger);
@@ -115,6 +120,10 @@ function registerTool(tool) {
115
120
  };
116
121
  }
117
122
  }
123
+ catch (err) {
124
+ logger(`${tool.name} error: ${err.message}`);
125
+ throw err;
126
+ }
118
127
  finally {
119
128
  guard.dispose();
120
129
  }
@@ -7,7 +7,7 @@ import { ToolCategories } from './categories.js';
7
7
  import { defineTool } from './ToolDefinition.js';
8
8
  export const consoleTool = defineTool({
9
9
  name: 'list_console_messages',
10
- description: 'List all console messages for the currently selected page',
10
+ description: 'List all console messages for the currently selected page since the last navigation.',
11
11
  annotations: {
12
12
  category: ToolCategories.DEBUGGING,
13
13
  readOnlyHint: true,
@@ -9,11 +9,12 @@ import { ToolCategories } from './categories.js';
9
9
  import { defineTool } from './ToolDefinition.js';
10
10
  const throttlingOptions = [
11
11
  'No emulation',
12
+ 'Offline',
12
13
  ...Object.keys(PredefinedNetworkConditions),
13
14
  ];
14
15
  export const emulateNetwork = defineTool({
15
16
  name: 'emulate_network',
16
- description: `Emulates network conditions such as throttling on the selected page.`,
17
+ description: `Emulates network conditions such as throttling or offline mode on the selected page.`,
17
18
  annotations: {
18
19
  category: ToolCategories.EMULATION,
19
20
  readOnlyHint: false,
@@ -21,7 +22,7 @@ export const emulateNetwork = defineTool({
21
22
  schema: {
22
23
  throttlingOption: z
23
24
  .enum(throttlingOptions)
24
- .describe(`The network throttling option to emulate. Available throttling options are: ${throttlingOptions.join(', ')}. Set to "No emulation" to disable.`),
25
+ .describe(`The network throttling option to emulate. Available throttling options are: ${throttlingOptions.join(', ')}. Set to "No emulation" to disable. Set to "Offline" to simulate offline network conditions.`),
25
26
  },
26
27
  handler: async (request, _response, context) => {
27
28
  const page = context.getSelectedPage();
@@ -31,6 +32,16 @@ export const emulateNetwork = defineTool({
31
32
  context.setNetworkConditions(null);
32
33
  return;
33
34
  }
35
+ if (conditions === 'Offline') {
36
+ await page.emulateNetworkConditions({
37
+ offline: true,
38
+ download: 0,
39
+ upload: 0,
40
+ latency: 0,
41
+ });
42
+ context.setNetworkConditions('Offline');
43
+ return;
44
+ }
34
45
  if (conditions in PredefinedNetworkConditions) {
35
46
  const networkCondition = PredefinedNetworkConditions[conditions];
36
47
  await page.emulateNetworkConditions(networkCondition);
@@ -29,7 +29,7 @@ const FILTERABLE_RESOURCE_TYPES = [
29
29
  ];
30
30
  export const listNetworkRequests = defineTool({
31
31
  name: 'list_network_requests',
32
- description: `List all requests for the currently selected page`,
32
+ description: `List all requests for the currently selected page since the last navigation.`,
33
33
  annotations: {
34
34
  category: ToolCategories.NETWORK,
35
35
  readOnlyHint: true,
@@ -124,7 +124,6 @@ async function stopTracingAndAppendOutput(page, response, context) {
124
124
  response.appendResponseLine('The performance trace has been stopped.');
125
125
  if (traceResultIsSuccess(result)) {
126
126
  context.storeTraceRecording(result);
127
- response.appendResponseLine('Here is a high level summary of the trace and the Insights that were found:');
128
127
  const traceSummaryText = getTraceSummary(result);
129
128
  response.appendResponseLine(traceSummaryText);
130
129
  }
@@ -53,15 +53,16 @@ const extraFormatDescriptions = `Information on performance traces may contain m
53
53
 
54
54
  ${PerformanceTraceFormatter.callFrameDataFormatDescription}
55
55
 
56
- ${PerformanceTraceFormatter.networkDataFormatDescription}
57
- `;
56
+ ${PerformanceTraceFormatter.networkDataFormatDescription}`;
58
57
  export function getTraceSummary(result) {
59
58
  const focus = AgentFocus.fromParsedTrace(result.parsedTrace);
60
59
  const formatter = new PerformanceTraceFormatter(focus);
61
- const output = formatter.formatTraceSummary();
62
- return `${extraFormatDescriptions}
60
+ const summaryText = formatter.formatTraceSummary();
61
+ return `## Summary of Performance trace findings:
62
+ ${summaryText}
63
63
 
64
- ${output}`;
64
+ ## Details on call tree & network request formats:
65
+ ${extraFormatDescriptions}`;
65
66
  }
66
67
  export function getInsightOutput(result, insightName) {
67
68
  if (!result.insights) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": "./build/src/index.js",
@@ -40,8 +40,9 @@
40
40
  "@modelcontextprotocol/sdk": "1.19.1",
41
41
  "core-js": "3.45.1",
42
42
  "debug": "4.4.3",
43
- "puppeteer-core": "24.23.0",
44
- "yargs": "18.0.0"
43
+ "puppeteer-core": "^24.24.0",
44
+ "yargs": "18.0.0",
45
+ "zod": "^3.25.76"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@eslint/js": "^9.35.0",
@@ -59,7 +60,7 @@
59
60
  "eslint-plugin-import": "^2.32.0",
60
61
  "globals": "^16.4.0",
61
62
  "prettier": "^3.6.2",
62
- "puppeteer": "24.23.0",
63
+ "puppeteer": "24.24.0",
63
64
  "sinon": "^21.0.0",
64
65
  "typescript": "^5.9.2",
65
66
  "typescript-eslint": "^8.43.0"