chrome-devtools-mcp 0.20.3 → 0.22.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 (55) hide show
  1. package/README.md +97 -20
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -49
  4. package/build/src/McpPage.js +16 -0
  5. package/build/src/McpResponse.js +220 -12
  6. package/build/src/PageCollector.js +14 -28
  7. package/build/src/WaitForHelper.js +31 -0
  8. package/build/src/bin/check-latest-version.js +25 -0
  9. package/build/src/bin/chrome-devtools-mcp-cli-options.js +28 -9
  10. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  11. package/build/src/bin/chrome-devtools-mcp.js +1 -0
  12. package/build/src/bin/chrome-devtools.js +9 -3
  13. package/build/src/bin/cliDefinitions.js +15 -9
  14. package/build/src/daemon/client.js +1 -1
  15. package/build/src/daemon/daemon.js +2 -6
  16. package/build/src/daemon/utils.js +1 -0
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +22 -1
  20. package/build/src/telemetry/ClearcutLogger.js +145 -6
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/types.js +5 -0
  24. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  25. package/build/src/third_party/THIRD_PARTY_NOTICES +1400 -483
  26. package/build/src/third_party/bundled-packages.json +6 -5
  27. package/build/src/third_party/devtools-formatter-worker.js +61 -66
  28. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  29. package/build/src/third_party/index.js +61622 -52803
  30. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md +1 -0
  31. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +10589 -4647
  32. package/build/src/tools/categories.js +5 -0
  33. package/build/src/tools/console.js +42 -39
  34. package/build/src/tools/emulation.js +1 -1
  35. package/build/src/tools/extensions.js +5 -11
  36. package/build/src/tools/inPage.js +105 -0
  37. package/build/src/tools/input.js +18 -16
  38. package/build/src/tools/lighthouse.js +3 -3
  39. package/build/src/tools/memory.js +50 -5
  40. package/build/src/tools/network.js +2 -2
  41. package/build/src/tools/pages.js +14 -6
  42. package/build/src/tools/performance.js +1 -1
  43. package/build/src/tools/screencast.js +2 -1
  44. package/build/src/tools/screenshot.js +3 -3
  45. package/build/src/tools/script.js +22 -16
  46. package/build/src/tools/tools.js +4 -0
  47. package/build/src/tools/webmcp.js +63 -0
  48. package/build/src/utils/check-for-updates.js +73 -0
  49. package/build/src/utils/files.js +4 -0
  50. package/build/src/utils/id.js +15 -0
  51. package/build/src/version.js +1 -1
  52. package/package.json +13 -9
  53. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorNoCorpCrossOriginNoCorsRequest.md +0 -3
  54. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorNoCorpCossOriginNoCorsRequest.md +0 -3
  55. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -9,7 +9,7 @@ import { defineTool, pageIdSchema } from './ToolDefinition.js';
9
9
  export const evaluateScript = defineTool(cliArgs => {
10
10
  return {
11
11
  name: 'evaluate_script',
12
- description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
12
+ description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON,
13
13
  so returned values have to be JSON-serializable.`,
14
14
  annotations: {
15
15
  category: ToolCategory.DEBUGGING,
@@ -32,18 +32,22 @@ Example with arguments: \`(el) => {
32
32
  .describe('The uid of an element on the page from the page content snapshot'))
33
33
  .optional()
34
34
  .describe(`An optional list of arguments to pass to the function.`),
35
+ dialogAction: zod
36
+ .string()
37
+ .optional()
38
+ .describe('Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.'),
35
39
  ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
36
40
  ...(cliArgs?.categoryExtensions
37
41
  ? {
38
42
  serviceWorkerId: zod
39
43
  .string()
40
44
  .optional()
41
- .describe(`An optional service worker id to evaluate the script in.`),
45
+ .describe(`The optional service worker id to evaluate the script in. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a service worker.`),
42
46
  }
43
47
  : {}),
44
48
  },
45
49
  handler: async (request, response, context) => {
46
- const { serviceWorkerId, args: uidArgs, function: fnString, pageId, } = request.params;
50
+ const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, } = request.params;
47
51
  if (cliArgs?.categoryExtensions && serviceWorkerId) {
48
52
  if (uidArgs && uidArgs.length > 0) {
49
53
  throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
@@ -52,7 +56,9 @@ Example with arguments: \`(el) => {
52
56
  throw new Error('specify either a pageId or a serviceWorkerId.');
53
57
  }
54
58
  const worker = await getWebWorker(context, serviceWorkerId);
55
- await performEvaluation(worker, fnString, [], response, context);
59
+ await context.getSelectedMcpPage().waitForEventsAfterAction(async () => {
60
+ await performEvaluation(worker, fnString, [], response);
61
+ }, { handleDialog: dialogAction ?? 'accept' });
56
62
  return;
57
63
  }
58
64
  const mcpPage = cliArgs?.experimentalPageIdRouting
@@ -68,7 +74,9 @@ Example with arguments: \`(el) => {
68
74
  args.push(handle);
69
75
  }
70
76
  const evaluatable = await getPageOrFrame(page, frames);
71
- await performEvaluation(evaluatable, fnString, args, response, context);
77
+ await mcpPage.waitForEventsAfterAction(async () => {
78
+ await performEvaluation(evaluatable, fnString, args, response);
79
+ }, { handleDialog: dialogAction ?? 'accept' });
72
80
  }
73
81
  finally {
74
82
  void Promise.allSettled(args.map(arg => arg.dispose()));
@@ -76,19 +84,17 @@ Example with arguments: \`(el) => {
76
84
  },
77
85
  };
78
86
  });
79
- const performEvaluation = async (evaluatable, fnString, args, response, context) => {
87
+ const performEvaluation = async (evaluatable, fnString, args, response) => {
80
88
  const fn = await evaluatable.evaluateHandle(`(${fnString})`);
81
89
  try {
82
- await context.waitForEventsAfterAction(async () => {
83
- const result = await evaluatable.evaluate(async (fn, ...args) => {
84
- // @ts-expect-error no types for function fn
85
- return JSON.stringify(await fn(...args));
86
- }, fn, ...args);
87
- response.appendResponseLine('Script ran on page and returned:');
88
- response.appendResponseLine('```json');
89
- response.appendResponseLine(`${result}`);
90
- response.appendResponseLine('```');
91
- });
90
+ const result = await evaluatable.evaluate(async (fn, ...args) => {
91
+ // @ts-expect-error no types for function fn
92
+ return JSON.stringify(await fn(...args));
93
+ }, fn, ...args);
94
+ response.appendResponseLine('Script ran on page and returned:');
95
+ response.appendResponseLine('```json');
96
+ response.appendResponseLine(`${result}`);
97
+ response.appendResponseLine('```');
92
98
  }
93
99
  finally {
94
100
  void fn.dispose();
@@ -6,6 +6,7 @@
6
6
  import * as consoleTools from './console.js';
7
7
  import * as emulationTools from './emulation.js';
8
8
  import * as extensionTools from './extensions.js';
9
+ import * as inPageTools from './inPage.js';
9
10
  import * as inputTools from './input.js';
10
11
  import * as lighthouseTools from './lighthouse.js';
11
12
  import * as memoryTools from './memory.js';
@@ -17,6 +18,7 @@ import * as screenshotTools from './screenshot.js';
17
18
  import * as scriptTools from './script.js';
18
19
  import * as slimTools from './slim/tools.js';
19
20
  import * as snapshotTools from './snapshot.js';
21
+ import * as webmcpTools from './webmcp.js';
20
22
  export const createTools = (args) => {
21
23
  const rawTools = args.slim
22
24
  ? Object.values(slimTools)
@@ -24,6 +26,7 @@ export const createTools = (args) => {
24
26
  ...Object.values(consoleTools),
25
27
  ...Object.values(emulationTools),
26
28
  ...Object.values(extensionTools),
29
+ ...Object.values(inPageTools),
27
30
  ...Object.values(inputTools),
28
31
  ...Object.values(lighthouseTools),
29
32
  ...Object.values(memoryTools),
@@ -34,6 +37,7 @@ export const createTools = (args) => {
34
37
  ...Object.values(screenshotTools),
35
38
  ...Object.values(scriptTools),
36
39
  ...Object.values(snapshotTools),
40
+ ...Object.values(webmcpTools),
37
41
  ];
38
42
  const tools = [];
39
43
  for (const tool of rawTools) {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { zod } from '../third_party/index.js';
7
+ import { ToolCategory } from './categories.js';
8
+ import { definePageTool } from './ToolDefinition.js';
9
+ export const listWebMcpTools = definePageTool({
10
+ name: 'list_webmcp_tools',
11
+ description: `Lists all WebMCP tools the page exposes.`,
12
+ annotations: {
13
+ category: ToolCategory.DEBUGGING,
14
+ readOnlyHint: true,
15
+ conditions: ['experimentalWebmcp'],
16
+ },
17
+ schema: {},
18
+ handler: async (_request, response, _context) => {
19
+ response.setListWebMcpTools();
20
+ },
21
+ });
22
+ export const executeWebMcpTool = definePageTool({
23
+ name: 'execute_webmcp_tool',
24
+ description: `Executes a WebMCP tool exposed by the page.`,
25
+ annotations: {
26
+ category: ToolCategory.DEBUGGING,
27
+ readOnlyHint: false,
28
+ conditions: ['experimentalWebmcp'],
29
+ },
30
+ schema: {
31
+ toolName: zod.string().describe('The name of the WebMCP tool to execute'),
32
+ input: zod
33
+ .string()
34
+ .optional()
35
+ .describe('The JSON-stringified parameters to pass to the WebMCP tool'),
36
+ },
37
+ handler: async (request, response) => {
38
+ const toolName = request.params.toolName;
39
+ let input = {};
40
+ if (request.params.input) {
41
+ try {
42
+ const parsed = JSON.parse(request.params.input);
43
+ if (typeof parsed === 'object' && parsed !== null) {
44
+ input = parsed;
45
+ }
46
+ else {
47
+ throw new Error('Parsed input is not an object');
48
+ }
49
+ }
50
+ catch (e) {
51
+ const errorMessage = e instanceof Error ? e.message : String(e);
52
+ throw new Error(`Failed to parse input as JSON: ${errorMessage}`);
53
+ }
54
+ }
55
+ const tools = request.page.pptrPage.webmcp.tools();
56
+ const tool = tools.find(t => t.name === toolName);
57
+ if (!tool) {
58
+ throw new Error(`Tool ${toolName} not found`);
59
+ }
60
+ const { status, output, errorText } = await tool.execute(input);
61
+ response.appendResponseLine(JSON.stringify({ status, output, errorText }, null, 2));
62
+ },
63
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import child_process from 'node:child_process';
7
+ import fs from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { semver } from '../third_party/index.js';
12
+ import { VERSION } from '../version.js';
13
+ /**
14
+ * Notifies the user if an update is available.
15
+ * @param message The message to display in the update notification.
16
+ */
17
+ let isChecking = false;
18
+ /** @internal Reset flag for tests only. */
19
+ export function resetUpdateCheckFlagForTesting() {
20
+ isChecking = false;
21
+ }
22
+ export async function checkForUpdates(message) {
23
+ if (isChecking || process.env['CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS']) {
24
+ return;
25
+ }
26
+ isChecking = true;
27
+ const cachePath = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'latest.json');
28
+ let cachedVersion;
29
+ let stats;
30
+ try {
31
+ stats = await fs.stat(cachePath);
32
+ const data = await fs.readFile(cachePath, 'utf8');
33
+ cachedVersion = JSON.parse(data).version;
34
+ }
35
+ catch {
36
+ // Ignore errors reading cache.
37
+ }
38
+ if (cachedVersion && semver.lt(VERSION, cachedVersion)) {
39
+ console.warn(`\nUpdate available: ${VERSION} -> ${cachedVersion}\n${message}\n`);
40
+ }
41
+ const now = Date.now();
42
+ if (stats && now - stats.mtimeMs < 24 * 60 * 60 * 1000) {
43
+ return;
44
+ }
45
+ // Update mtime immediately to prevent multiple subprocesses.
46
+ try {
47
+ const parentDir = path.dirname(cachePath);
48
+ await fs.mkdir(parentDir, { recursive: true });
49
+ const nowTime = new Date();
50
+ if (stats) {
51
+ await fs.utimes(cachePath, nowTime, nowTime);
52
+ }
53
+ else {
54
+ await fs.writeFile(cachePath, JSON.stringify({ version: VERSION }));
55
+ }
56
+ }
57
+ catch {
58
+ // Ignore errors.
59
+ }
60
+ // In a separate process, check the latest available version number
61
+ // and update the local snapshot accordingly.
62
+ const scriptPath = path.join(import.meta.dirname, '..', 'bin', 'check-latest-version.js');
63
+ try {
64
+ const child = child_process.spawn(process.execPath, [scriptPath, cachePath], {
65
+ detached: true,
66
+ stdio: 'ignore',
67
+ });
68
+ child.unref();
69
+ }
70
+ catch {
71
+ // Fail silently in case of any errors.
72
+ }
73
+ }
@@ -17,3 +17,7 @@ export async function saveTemporaryFile(data, filename) {
17
17
  throw new Error('Could not save a file', { cause: err });
18
18
  }
19
19
  }
20
+ export function ensureExtension(filepath, extension) {
21
+ const ext = path.extname(filepath);
22
+ return filepath.slice(0, filepath.length - ext.length) + extension;
23
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export function createIdGenerator() {
7
+ let i = 1;
8
+ return () => {
9
+ if (i === Number.MAX_SAFE_INTEGER) {
10
+ i = 0;
11
+ }
12
+ return i++;
13
+ };
14
+ }
15
+ export const stableIdSymbol = Symbol('stableIdSymbol');
@@ -5,5 +5,5 @@
5
5
  */
6
6
  // If moved update release-please config
7
7
  // x-release-please-start-version
8
- export const VERSION = '0.20.3';
8
+ export const VERSION = '0.22.0';
9
9
  // x-release-please-end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.20.3",
3
+ "version": "0.22.0",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "format": "eslint --cache --fix . && prettier --write --cache .",
18
18
  "check-format": "eslint --cache . && prettier --check --cache .;",
19
- "gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run format",
19
+ "gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-tool-call-metrics && npm run update-flag-usage-metrics && npm run format",
20
20
  "docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
21
21
  "start": "npm run build && node build/src/index.js",
22
22
  "start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
@@ -27,6 +27,8 @@
27
27
  "prepare": "node --experimental-strip-types scripts/prepare.ts",
28
28
  "verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts",
29
29
  "update-lighthouse": "node --experimental-strip-types scripts/update-lighthouse.ts",
30
+ "update-tool-call-metrics": "node --experimental-strip-types scripts/update_tool_call_metrics.ts",
31
+ "update-flag-usage-metrics": "node --experimental-strip-types scripts/update_flag_usage_metrics.ts",
30
32
  "verify-npm-package": "node scripts/verify-npm-package.mjs",
31
33
  "eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts",
32
34
  "count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
@@ -47,7 +49,7 @@
47
49
  "devDependencies": {
48
50
  "@eslint/js": "^9.35.0",
49
51
  "@google/genai": "^1.37.0",
50
- "@modelcontextprotocol/sdk": "1.27.1",
52
+ "@modelcontextprotocol/sdk": "1.29.0",
51
53
  "@rollup/plugin-commonjs": "^29.0.0",
52
54
  "@rollup/plugin-json": "^6.1.0",
53
55
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -55,26 +57,28 @@
55
57
  "@types/debug": "^4.1.12",
56
58
  "@types/filesystem": "^0.0.36",
57
59
  "@types/node": "^25.0.0",
60
+ "@types/semver": "^7.7.1",
58
61
  "@types/sinon": "^21.0.0",
59
62
  "@types/yargs": "^17.0.33",
60
63
  "@typescript-eslint/eslint-plugin": "^8.43.0",
61
64
  "@typescript-eslint/parser": "^8.43.0",
62
- "chrome-devtools-frontend": "1.0.1599001",
63
- "core-js": "3.48.0",
65
+ "chrome-devtools-frontend": "1.0.1613625",
66
+ "core-js": "3.49.0",
64
67
  "debug": "4.4.3",
65
68
  "eslint": "^9.35.0",
66
69
  "eslint-import-resolver-typescript": "^4.4.4",
67
70
  "eslint-plugin-import": "^2.32.0",
68
71
  "globals": "^17.0.0",
69
- "lighthouse": "13.0.3",
72
+ "lighthouse": "13.1.0",
70
73
  "prettier": "^3.6.2",
71
- "puppeteer": "24.39.1",
72
- "rollup": "4.59.0",
74
+ "puppeteer": "24.42.0",
75
+ "rollup": "4.60.2",
73
76
  "rollup-plugin-cleanup": "^3.2.1",
74
77
  "rollup-plugin-license": "^3.6.0",
78
+ "semver": "^7.7.4",
75
79
  "sinon": "^21.0.0",
76
80
  "tiktoken": "^1.0.22",
77
- "typescript": "^5.9.2",
81
+ "typescript": "^6.0.2",
78
82
  "typescript-eslint": "^8.43.0",
79
83
  "yargs": "18.0.0"
80
84
  },
@@ -1,3 +0,0 @@
1
- # Dictionary compression cannot be used for cross-origin no-cors requests without a Cross-Origin-Resource-Policy response header
2
-
3
- To use dictionary compression for a cross-origin no-cors request, the response must have a `Cross-Origin-Resource-Policy: cross-origin` header. Without it, the response can not be decoded and will fail.
@@ -1,3 +0,0 @@
1
- # A cross-origin no-cors response cannot be stored as a shared dictionary for future requests without a Cross-Origin-Resource-Policy response header
2
-
3
- To store a response from a cross-origin no-cors request as a shared dictionary for future requests, the response must have a `Cross-Origin-Resource-Policy: cross-origin` header. Without it, the response will not be stored as a dictionary.
@@ -1,35 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
- export class ExtensionRegistry {
9
- #extensions = new Map();
10
- async registerExtension(id, extensionPath) {
11
- const manifestPath = path.join(extensionPath, 'manifest.json');
12
- const manifestContent = await fs.readFile(manifestPath, 'utf-8');
13
- const manifest = JSON.parse(manifestContent);
14
- const name = manifest.name ?? 'Unknown';
15
- const version = manifest.version ?? 'Unknown';
16
- const extension = {
17
- id,
18
- name,
19
- version,
20
- isEnabled: true,
21
- path: extensionPath,
22
- };
23
- this.#extensions.set(extension.id, extension);
24
- return extension;
25
- }
26
- remove(id) {
27
- this.#extensions.delete(id);
28
- }
29
- list() {
30
- return Array.from(this.#extensions.values());
31
- }
32
- getById(id) {
33
- return this.#extensions.get(id);
34
- }
35
- }