chrome-devtools-mcp 0.18.1 → 0.20.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 (44) hide show
  1. package/README.md +6 -5
  2. package/build/src/McpContext.js +242 -266
  3. package/build/src/McpPage.js +95 -0
  4. package/build/src/McpResponse.js +124 -48
  5. package/build/src/bin/chrome-devtools-cli-options.js +651 -0
  6. package/build/src/{cli.js → bin/chrome-devtools-mcp-cli-options.js} +12 -2
  7. package/build/src/bin/chrome-devtools-mcp-main.js +35 -0
  8. package/build/src/bin/chrome-devtools-mcp.js +21 -0
  9. package/build/src/bin/chrome-devtools.js +185 -0
  10. package/build/src/bin/cliDefinitions.js +615 -0
  11. package/build/src/browser.js +13 -12
  12. package/build/src/daemon/client.js +152 -0
  13. package/build/src/daemon/daemon.js +56 -17
  14. package/build/src/daemon/types.js +6 -0
  15. package/build/src/daemon/utils.js +57 -16
  16. package/build/src/index.js +204 -16
  17. package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
  18. package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
  19. package/build/src/third_party/bundled-packages.json +4 -3
  20. package/build/src/third_party/devtools-formatter-worker.js +5 -14
  21. package/build/src/third_party/index.js +2128 -472
  22. package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +7 -0
  23. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
  24. package/build/src/tools/ToolDefinition.js +52 -0
  25. package/build/src/tools/console.js +3 -3
  26. package/build/src/tools/emulation.js +13 -45
  27. package/build/src/tools/extensions.js +17 -0
  28. package/build/src/tools/input.js +33 -33
  29. package/build/src/tools/lighthouse.js +123 -0
  30. package/build/src/tools/memory.js +5 -5
  31. package/build/src/tools/network.js +7 -7
  32. package/build/src/tools/pages.js +32 -32
  33. package/build/src/tools/performance.js +16 -14
  34. package/build/src/tools/screencast.js +5 -5
  35. package/build/src/tools/screenshot.js +6 -6
  36. package/build/src/tools/script.js +99 -49
  37. package/build/src/tools/slim/tools.js +18 -18
  38. package/build/src/tools/snapshot.js +5 -4
  39. package/build/src/tools/tools.js +2 -0
  40. package/build/src/types.js +6 -0
  41. package/build/src/utils/files.js +19 -0
  42. package/build/src/version.js +1 -1
  43. package/package.json +15 -9
  44. package/build/src/main.js +0 -203
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import net from 'node:net';
9
+ import { logger } from '../logger.js';
10
+ import { PipeTransport } from '../third_party/index.js';
11
+ import { saveTemporaryFile } from '../utils/files.js';
12
+ import { DAEMON_SCRIPT_PATH, getSocketPath, getPidFilePath, isDaemonRunning, } from './utils.js';
13
+ const FILE_TIMEOUT = 10_000;
14
+ /**
15
+ * Waits for a file to be created and populated (removed = false) or removed (removed = true).
16
+ */
17
+ function waitForFile(filePath, removed = false) {
18
+ return new Promise((resolve, reject) => {
19
+ const check = () => {
20
+ const exists = fs.existsSync(filePath);
21
+ if (removed) {
22
+ return !exists;
23
+ }
24
+ if (!exists) {
25
+ return false;
26
+ }
27
+ try {
28
+ return fs.statSync(filePath).size > 0;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ };
34
+ if (check()) {
35
+ resolve();
36
+ return;
37
+ }
38
+ const timer = setTimeout(() => {
39
+ fs.unwatchFile(filePath);
40
+ reject(new Error(`Timeout: file ${filePath} ${removed ? 'not removed' : 'not found'} within ${FILE_TIMEOUT}ms`));
41
+ }, FILE_TIMEOUT);
42
+ fs.watchFile(filePath, { interval: 500 }, () => {
43
+ if (check()) {
44
+ clearTimeout(timer);
45
+ fs.unwatchFile(filePath);
46
+ resolve();
47
+ }
48
+ });
49
+ });
50
+ }
51
+ export async function startDaemon(mcpArgs = []) {
52
+ if (isDaemonRunning()) {
53
+ logger('Daemon is already running');
54
+ return;
55
+ }
56
+ const pidFilePath = getPidFilePath();
57
+ if (fs.existsSync(pidFilePath)) {
58
+ fs.unlinkSync(pidFilePath);
59
+ }
60
+ logger('Starting daemon...', ...mcpArgs);
61
+ const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ env: process.env,
65
+ cwd: process.cwd(),
66
+ windowsHide: true,
67
+ });
68
+ child.unref();
69
+ await waitForFile(pidFilePath);
70
+ }
71
+ const SEND_COMMAND_TIMEOUT = 60_000; // ms
72
+ /**
73
+ * `sendCommand` opens a socket connection sends a single command and disconnects.
74
+ */
75
+ export async function sendCommand(command) {
76
+ const socketPath = getSocketPath();
77
+ const socket = net.createConnection({
78
+ path: socketPath,
79
+ });
80
+ return new Promise((resolve, reject) => {
81
+ const timer = setTimeout(() => {
82
+ socket.destroy();
83
+ reject(new Error('Timeout waiting for daemon response'));
84
+ }, SEND_COMMAND_TIMEOUT);
85
+ const transport = new PipeTransport(socket, socket);
86
+ transport.onmessage = async (message) => {
87
+ clearTimeout(timer);
88
+ logger('onmessage', message);
89
+ resolve(JSON.parse(message));
90
+ };
91
+ socket.on('error', error => {
92
+ clearTimeout(timer);
93
+ logger('Socket error:', error);
94
+ reject(error);
95
+ });
96
+ socket.on('close', () => {
97
+ clearTimeout(timer);
98
+ logger('Socket closed:');
99
+ reject(new Error('Socket closed'));
100
+ });
101
+ logger('Sending message', command);
102
+ transport.send(JSON.stringify(command));
103
+ });
104
+ }
105
+ export async function stopDaemon() {
106
+ if (!isDaemonRunning()) {
107
+ logger('Daemon is not running');
108
+ return;
109
+ }
110
+ const pidFilePath = getPidFilePath();
111
+ await sendCommand({ method: 'stop' });
112
+ await waitForFile(pidFilePath, /*removed=*/ true);
113
+ }
114
+ export async function handleResponse(response, format) {
115
+ if (response.isError) {
116
+ return JSON.stringify(response.content);
117
+ }
118
+ if (format === 'json') {
119
+ if (response.structuredContent) {
120
+ return JSON.stringify(response.structuredContent);
121
+ }
122
+ // Fall-through to text for backward compatibility.
123
+ }
124
+ const chunks = [];
125
+ for (const content of response.content) {
126
+ if (content.type === 'text') {
127
+ chunks.push(content.text);
128
+ }
129
+ else if (content.type === 'image') {
130
+ const imageData = content.data;
131
+ const mimeType = content.mimeType;
132
+ let extension = '.png';
133
+ switch (mimeType) {
134
+ case 'image/jpg':
135
+ case 'image/jpeg':
136
+ extension = '.jpeg';
137
+ break;
138
+ case 'webp':
139
+ extension = '.webp';
140
+ break;
141
+ }
142
+ const data = Buffer.from(imageData, 'base64');
143
+ const name = crypto.randomUUID();
144
+ const { filepath } = await saveTemporaryFile(data, `${name}${extension}`);
145
+ chunks.push(`Saved to ${filepath}.`);
146
+ }
147
+ else {
148
+ throw new Error('Not supported response content type');
149
+ }
150
+ }
151
+ return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
152
+ }
@@ -4,27 +4,41 @@
4
4
  * Copyright 2026 Google LLC
5
5
  * SPDX-License-Identifier: Apache-2.0
6
6
  */
7
- import fs from 'node:fs/promises';
7
+ import fs from 'node:fs';
8
8
  import { createServer } from 'node:net';
9
+ import path from 'node:path';
9
10
  import process from 'node:process';
10
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
11
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
12
11
  import { logger } from '../logger.js';
13
- import { PipeTransport } from '../third_party/index.js';
12
+ import { Client, PipeTransport, StdioClientTransport, } from '../third_party/index.js';
14
13
  import { VERSION } from '../version.js';
15
- import { getSocketPath, handlePidFile, INDEX_SCRIPT_PATH, IS_WINDOWS, } from './utils.js';
16
- const pidFile = handlePidFile();
14
+ import { getDaemonPid, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
+ const pid = getDaemonPid();
16
+ if (isDaemonRunning(pid)) {
17
+ logger('Another daemon process is running.');
18
+ process.exit(1);
19
+ }
20
+ const pidFilePath = getPidFilePath();
21
+ fs.mkdirSync(path.dirname(pidFilePath), {
22
+ recursive: true,
23
+ });
24
+ fs.writeFileSync(pidFilePath, process.pid.toString());
25
+ logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
17
26
  const socketPath = getSocketPath();
27
+ const startDate = new Date();
28
+ const mcpServerArgs = process.argv.slice(2);
18
29
  let mcpClient = null;
19
30
  let mcpTransport = null;
20
31
  let server = null;
21
32
  async function setupMCPClient() {
22
33
  console.log('Setting up MCP client connection...');
23
- const args = process.argv.slice(2);
24
34
  // Create stdio transport for chrome-devtools-mcp
35
+ // Workaround for https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/client/stdio.ts#L128
36
+ // which causes the console window to show on Windows.
37
+ // @ts-expect-error no types for type.
38
+ process.type = 'mcp-client';
25
39
  mcpTransport = new StdioClientTransport({
26
40
  command: process.execPath,
27
- args: [INDEX_SCRIPT_PATH, ...args],
41
+ args: [INDEX_SCRIPT_PATH, ...mcpServerArgs],
28
42
  env: process.env,
29
43
  });
30
44
  mcpClient = new Client({
@@ -53,7 +67,9 @@ async function handleRequest(msg) {
53
67
  };
54
68
  }
55
69
  else if (msg.method === 'stop') {
56
- // Trigger cleanup asynchronously
70
+ // Ensure we are not interrupting in-progress starting.
71
+ await started;
72
+ // Trigger cleanup asynchronously.
57
73
  setImmediate(() => {
58
74
  void cleanup();
59
75
  });
@@ -62,7 +78,19 @@ async function handleRequest(msg) {
62
78
  message: 'stopping',
63
79
  };
64
80
  }
65
- else {
81
+ else if (msg.method === 'status') {
82
+ return {
83
+ success: true,
84
+ result: JSON.stringify({
85
+ pid: process.pid,
86
+ socketPath,
87
+ startDate: startDate.toISOString(),
88
+ version: VERSION,
89
+ args: mcpServerArgs,
90
+ }),
91
+ };
92
+ }
93
+ {
66
94
  return {
67
95
  success: false,
68
96
  error: `Unknown method: ${JSON.stringify(msg, null, 2)}`,
@@ -81,7 +109,7 @@ async function startSocketServer() {
81
109
  // Remove existing socket file if it exists (only on non-Windows)
82
110
  if (!IS_WINDOWS) {
83
111
  try {
84
- await fs.unlink(socketPath);
112
+ fs.unlinkSync(socketPath);
85
113
  }
86
114
  catch {
87
115
  // ignore errors.
@@ -135,12 +163,23 @@ async function cleanup() {
135
163
  catch (error) {
136
164
  logger('Error closing MCP transport:', error);
137
165
  }
138
- server?.close(() => {
139
- if (!IS_WINDOWS) {
140
- void fs.unlink(socketPath).catch(() => undefined);
166
+ if (server) {
167
+ await new Promise(resolve => {
168
+ server.close(() => resolve());
169
+ });
170
+ }
171
+ if (!IS_WINDOWS) {
172
+ try {
173
+ fs.unlinkSync(socketPath);
141
174
  }
142
- });
143
- await fs.unlink(pidFile).catch(() => undefined);
175
+ catch {
176
+ // ignore errors
177
+ }
178
+ }
179
+ logger(`unlinking ${pidFilePath}`);
180
+ if (fs.existsSync(pidFilePath)) {
181
+ fs.unlinkSync(pidFilePath);
182
+ }
144
183
  process.exit(0);
145
184
  }
146
185
  // Handle shutdown signals
@@ -161,7 +200,7 @@ process.on('unhandledRejection', error => {
161
200
  logger('Unhandled rejection:', error);
162
201
  });
163
202
  // Start the server
164
- startSocketServer().catch(error => {
203
+ const started = startSocketServer().catch(error => {
165
204
  logger('Failed to start daemon server:', error);
166
205
  process.exit(1);
167
206
  });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
@@ -7,8 +7,9 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import process from 'node:process';
10
+ import { logger } from '../logger.js';
10
11
  export const DAEMON_SCRIPT_PATH = path.join(import.meta.dirname, 'daemon.js');
11
- export const INDEX_SCRIPT_PATH = path.join(import.meta.dirname, '..', 'index.js');
12
+ export const INDEX_SCRIPT_PATH = path.join(import.meta.dirname, '..', 'bin', 'chrome-devtools-mcp.js');
12
13
  const APP_NAME = 'chrome-devtools-mcp';
13
14
  // Using these paths due to strict limits on the POSIX socket path length.
14
15
  export function getSocketPath() {
@@ -43,25 +44,65 @@ export function getRuntimeHome() {
43
44
  return path.join(os.tmpdir(), APP_NAME);
44
45
  }
45
46
  export const IS_WINDOWS = os.platform() === 'win32';
46
- export function handlePidFile() {
47
+ export function getPidFilePath() {
47
48
  const runtimeDir = getRuntimeHome();
48
- const pidPath = path.join(runtimeDir, 'daemon.pid');
49
- if (fs.existsSync(pidPath)) {
50
- const oldPid = parseInt(fs.readFileSync(pidPath, 'utf8'), 10);
49
+ return path.join(runtimeDir, 'daemon.pid');
50
+ }
51
+ export function getDaemonPid() {
52
+ try {
53
+ const pidFile = getPidFilePath();
54
+ logger(`Daemon pid file ${pidFile}`);
55
+ if (!fs.existsSync(pidFile)) {
56
+ return null;
57
+ }
58
+ const pidContent = fs.readFileSync(pidFile, 'utf-8');
59
+ const pid = parseInt(pidContent.trim(), 10);
60
+ logger(`Daemon pid: ${pid}`);
61
+ if (isNaN(pid)) {
62
+ return null;
63
+ }
64
+ return pid;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function isDaemonRunning(pid = getDaemonPid()) {
71
+ if (pid) {
51
72
  try {
52
- // Sending signal 0 checks if the process is still alive without killing it
53
- process.kill(oldPid, 0);
54
- console.error('Daemon is already running!');
55
- process.exit(1);
73
+ process.kill(pid, 0); // Throws if process doesn't exist
74
+ return true;
56
75
  }
57
76
  catch {
58
- // Process is dead, we can safely overwrite the PID file
59
- fs.unlinkSync(pidPath);
77
+ // Process is dead, stale PID file. Proceed with startup.
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ export function serializeArgs(options, argv) {
83
+ const args = [];
84
+ for (const key of Object.keys(options)) {
85
+ if (argv[key] === undefined || argv[key] === null) {
86
+ continue;
87
+ }
88
+ const value = argv[key];
89
+ const kebabKey = key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
90
+ if (typeof value === 'boolean') {
91
+ if (value) {
92
+ args.push(`--${kebabKey}`);
93
+ }
94
+ else {
95
+ args.push(`--no-${kebabKey}`);
96
+ }
97
+ }
98
+ else if (Array.isArray(value)) {
99
+ for (const item of value) {
100
+ args.push(`--${kebabKey}`, String(item));
101
+ }
102
+ }
103
+ else {
104
+ args.push(`--${kebabKey}`, String(value));
60
105
  }
61
106
  }
62
- fs.mkdirSync(path.dirname(pidPath), {
63
- recursive: true,
64
- });
65
- fs.writeFileSync(pidPath, process.pid.toString());
66
- return pidPath;
107
+ return args;
67
108
  }
@@ -1,21 +1,209 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * @license
4
- * Copyright 2025 Google LLC
3
+ * Copyright 2026 Google LLC
5
4
  * SPDX-License-Identifier: Apache-2.0
6
5
  */
7
- import { version } from 'node:process';
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);
6
+ import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
7
+ import { loadIssueDescriptions } from './issue-descriptions.js';
8
+ import { logger } from './logger.js';
9
+ import { McpContext } from './McpContext.js';
10
+ import { McpResponse } from './McpResponse.js';
11
+ import { Mutex } from './Mutex.js';
12
+ import { SlimMcpResponse } from './SlimMcpResponse.js';
13
+ import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
14
+ import { bucketizeLatency } from './telemetry/metricUtils.js';
15
+ import { McpServer, SetLevelRequestSchema, } from './third_party/index.js';
16
+ import { ToolCategory } from './tools/categories.js';
17
+ import { pageIdSchema } from './tools/ToolDefinition.js';
18
+ import { createTools } from './tools/tools.js';
19
+ import { VERSION } from './version.js';
20
+ export async function createMcpServer(serverArgs, options) {
21
+ let clearcutLogger;
22
+ if (serverArgs.usageStatistics) {
23
+ clearcutLogger = new ClearcutLogger({
24
+ logFile: serverArgs.logFile,
25
+ appVersion: VERSION,
26
+ clearcutEndpoint: serverArgs.clearcutEndpoint,
27
+ clearcutForceFlushIntervalMs: serverArgs.clearcutForceFlushIntervalMs,
28
+ clearcutIncludePidHeader: serverArgs.clearcutIncludePidHeader,
29
+ });
30
+ }
31
+ const server = new McpServer({
32
+ name: 'chrome_devtools',
33
+ title: 'Chrome DevTools MCP server',
34
+ version: VERSION,
35
+ }, { capabilities: { logging: {} } });
36
+ server.server.setRequestHandler(SetLevelRequestSchema, () => {
37
+ return {};
38
+ });
39
+ let context;
40
+ async function getContext() {
41
+ const chromeArgs = (serverArgs.chromeArg ?? []).map(String);
42
+ const ignoreDefaultChromeArgs = (serverArgs.ignoreDefaultChromeArg ?? []).map(String);
43
+ if (serverArgs.proxyServer) {
44
+ chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`);
45
+ }
46
+ const devtools = serverArgs.experimentalDevtools ?? false;
47
+ const browser = serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect
48
+ ? await ensureBrowserConnected({
49
+ browserURL: serverArgs.browserUrl,
50
+ wsEndpoint: serverArgs.wsEndpoint,
51
+ wsHeaders: serverArgs.wsHeaders,
52
+ // Important: only pass channel, if autoConnect is true.
53
+ channel: serverArgs.autoConnect
54
+ ? serverArgs.channel
55
+ : undefined,
56
+ userDataDir: serverArgs.userDataDir,
57
+ devtools,
58
+ })
59
+ : await ensureBrowserLaunched({
60
+ headless: serverArgs.headless,
61
+ executablePath: serverArgs.executablePath,
62
+ channel: serverArgs.channel,
63
+ isolated: serverArgs.isolated ?? false,
64
+ userDataDir: serverArgs.userDataDir,
65
+ logFile: options.logFile,
66
+ viewport: serverArgs.viewport,
67
+ chromeArgs,
68
+ ignoreDefaultChromeArgs,
69
+ acceptInsecureCerts: serverArgs.acceptInsecureCerts,
70
+ devtools,
71
+ enableExtensions: serverArgs.categoryExtensions,
72
+ viaCli: serverArgs.viaCli,
73
+ });
74
+ if (context?.browser !== browser) {
75
+ context = await McpContext.from(browser, logger, {
76
+ experimentalDevToolsDebugging: devtools,
77
+ experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
78
+ performanceCrux: serverArgs.performanceCrux,
79
+ });
80
+ }
81
+ return context;
82
+ }
83
+ const toolMutex = new Mutex();
84
+ function registerTool(tool) {
85
+ if (tool.annotations.category === ToolCategory.EMULATION &&
86
+ serverArgs.categoryEmulation === false) {
87
+ return;
88
+ }
89
+ if (tool.annotations.category === ToolCategory.PERFORMANCE &&
90
+ serverArgs.categoryPerformance === false) {
91
+ return;
92
+ }
93
+ if (tool.annotations.category === ToolCategory.NETWORK &&
94
+ serverArgs.categoryNetwork === false) {
95
+ return;
96
+ }
97
+ if (tool.annotations.category === ToolCategory.EXTENSIONS &&
98
+ serverArgs.categoryExtensions === false) {
99
+ return;
100
+ }
101
+ if (tool.annotations.conditions?.includes('computerVision') &&
102
+ !serverArgs.experimentalVision) {
103
+ return;
104
+ }
105
+ if (tool.annotations.conditions?.includes('experimentalInteropTools') &&
106
+ !serverArgs.experimentalInteropTools) {
107
+ return;
108
+ }
109
+ if (tool.annotations.conditions?.includes('screencast') &&
110
+ !serverArgs.experimentalScreencast) {
111
+ return;
112
+ }
113
+ const schema = 'pageScoped' in tool &&
114
+ tool.pageScoped &&
115
+ serverArgs.experimentalPageIdRouting &&
116
+ !serverArgs.slim
117
+ ? { ...tool.schema, ...pageIdSchema }
118
+ : tool.schema;
119
+ server.registerTool(tool.name, {
120
+ description: tool.description,
121
+ inputSchema: schema,
122
+ annotations: tool.annotations,
123
+ }, async (params) => {
124
+ const guard = await toolMutex.acquire();
125
+ const startTime = Date.now();
126
+ let success = false;
127
+ try {
128
+ logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
129
+ const context = await getContext();
130
+ logger(`${tool.name} context: resolved`);
131
+ await context.detectOpenDevToolsWindows();
132
+ const response = serverArgs.slim
133
+ ? new SlimMcpResponse(serverArgs)
134
+ : new McpResponse(serverArgs);
135
+ if ('pageScoped' in tool && tool.pageScoped) {
136
+ const page = serverArgs.experimentalPageIdRouting &&
137
+ params.pageId &&
138
+ !serverArgs.slim
139
+ ? context.getPageById(params.pageId)
140
+ : context.getSelectedMcpPage();
141
+ response.setPage(page);
142
+ await tool.handler({
143
+ params,
144
+ page,
145
+ }, response, context);
146
+ }
147
+ else {
148
+ await tool.handler(
149
+ // @ts-expect-error types do not match.
150
+ {
151
+ params,
152
+ }, response, context);
153
+ }
154
+ const { content, structuredContent } = await response.handle(tool.name, context);
155
+ const result = {
156
+ content,
157
+ };
158
+ success = true;
159
+ if (serverArgs.experimentalStructuredContent) {
160
+ result.structuredContent = structuredContent;
161
+ }
162
+ return result;
163
+ }
164
+ catch (err) {
165
+ logger(`${tool.name} error:`, err, err?.stack);
166
+ let errorText = err && 'message' in err ? err.message : String(err);
167
+ if ('cause' in err && err.cause) {
168
+ errorText += `\nCause: ${err.cause.message}`;
169
+ }
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text',
174
+ text: errorText,
175
+ },
176
+ ],
177
+ isError: true,
178
+ };
179
+ }
180
+ finally {
181
+ void clearcutLogger?.logToolInvocation({
182
+ toolName: tool.name,
183
+ success,
184
+ latencyMs: bucketizeLatency(Date.now() - startTime),
185
+ });
186
+ guard.dispose();
187
+ }
188
+ });
189
+ }
190
+ const tools = createTools(serverArgs);
191
+ for (const tool of tools) {
192
+ registerTool(tool);
193
+ }
194
+ await loadIssueDescriptions();
195
+ return { server, clearcutLogger };
12
196
  }
13
- if (major === 22 && minor < 12) {
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.`);
15
- process.exit(1);
16
- }
17
- if (major < 20) {
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.`);
19
- process.exit(1);
20
- }
21
- await import('./main.js');
197
+ export const logDisclaimers = (args) => {
198
+ console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
199
+ debug, and modify any data in the browser or DevTools.
200
+ Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
201
+ if (!args.slim && args.performanceCrux) {
202
+ console.error(`Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data. To disable, run with --no-performance-crux.`);
203
+ }
204
+ if (!args.slim && args.usageStatistics) {
205
+ console.error(`
206
+ Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
207
+ For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics`);
208
+ }
209
+ };
@@ -125,6 +125,7 @@ export class ClearcutSender {
125
125
  });
126
126
  }
127
127
  #scheduleFlush(delayMs) {
128
+ logger(`Scheduling flush in ${delayMs}`);
128
129
  if (this.#flushTimer) {
129
130
  clearTimeout(this.#flushTimer);
130
131
  }
@@ -135,6 +136,7 @@ export class ClearcutSender {
135
136
  }, delayMs);
136
137
  }
137
138
  async #sendBatch(events) {
139
+ logger(`Sending batch of ${events.length}`);
138
140
  const requestBody = {
139
141
  log_source: LOG_SOURCE,
140
142
  request_time_ms: Date.now().toString(),