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.
- package/README.md +6 -5
- package/build/src/McpContext.js +242 -266
- package/build/src/McpPage.js +95 -0
- package/build/src/McpResponse.js +124 -48
- package/build/src/bin/chrome-devtools-cli-options.js +651 -0
- package/build/src/{cli.js → bin/chrome-devtools-mcp-cli-options.js} +12 -2
- package/build/src/bin/chrome-devtools-mcp-main.js +35 -0
- package/build/src/bin/chrome-devtools-mcp.js +21 -0
- package/build/src/bin/chrome-devtools.js +185 -0
- package/build/src/bin/cliDefinitions.js +615 -0
- package/build/src/browser.js +13 -12
- package/build/src/daemon/client.js +152 -0
- package/build/src/daemon/daemon.js +56 -17
- package/build/src/daemon/types.js +6 -0
- package/build/src/daemon/utils.js +57 -16
- package/build/src/index.js +204 -16
- package/build/src/telemetry/watchdog/ClearcutSender.js +2 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +1480 -111
- package/build/src/third_party/bundled-packages.json +4 -3
- package/build/src/third_party/devtools-formatter-worker.js +5 -14
- package/build/src/third_party/index.js +2128 -472
- package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +7 -0
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +54183 -0
- package/build/src/tools/ToolDefinition.js +52 -0
- package/build/src/tools/console.js +3 -3
- package/build/src/tools/emulation.js +13 -45
- package/build/src/tools/extensions.js +17 -0
- package/build/src/tools/input.js +33 -33
- package/build/src/tools/lighthouse.js +123 -0
- package/build/src/tools/memory.js +5 -5
- package/build/src/tools/network.js +7 -7
- package/build/src/tools/pages.js +32 -32
- package/build/src/tools/performance.js +16 -14
- package/build/src/tools/screencast.js +5 -5
- package/build/src/tools/screenshot.js +6 -6
- package/build/src/tools/script.js +99 -49
- package/build/src/tools/slim/tools.js +18 -18
- package/build/src/tools/snapshot.js +5 -4
- package/build/src/tools/tools.js +2 -0
- package/build/src/types.js +6 -0
- package/build/src/utils/files.js +19 -0
- package/build/src/version.js +1 -1
- package/package.json +15 -9
- 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
|
|
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 {
|
|
16
|
-
const
|
|
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, ...
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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, '..', '
|
|
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
|
|
47
|
+
export function getPidFilePath() {
|
|
47
48
|
const runtimeDir = getRuntimeHome();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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,
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
recursive: true,
|
|
64
|
-
});
|
|
65
|
-
fs.writeFileSync(pidPath, process.pid.toString());
|
|
66
|
-
return pidPath;
|
|
107
|
+
return args;
|
|
67
108
|
}
|
package/build/src/index.js
CHANGED
|
@@ -1,21 +1,209 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* @license
|
|
4
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
5
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
6
5
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
console.error(`
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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(),
|