chrome-devtools-mcp 0.17.3 → 0.18.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 +27 -2
- package/build/src/McpContext.js +174 -74
- package/build/src/McpResponse.js +11 -2
- package/build/src/SlimMcpResponse.js +18 -0
- package/build/src/browser.js +20 -0
- package/build/src/cli.js +12 -0
- package/build/src/daemon/daemon.js +167 -0
- package/build/src/daemon/utils.js +67 -0
- package/build/src/formatters/ConsoleFormatter.js +106 -105
- package/build/src/formatters/IssueFormatter.js +50 -48
- package/build/src/main.js +16 -11
- package/build/src/third_party/THIRD_PARTY_NOTICES +1 -1
- package/build/src/third_party/bundled-packages.json +2 -2
- package/build/src/third_party/index.js +107 -28
- package/build/src/third_party/issue-descriptions/corsLocalNetworkAccessPermissionDenied.md +2 -2
- package/build/src/tools/emulation.js +1 -83
- package/build/src/tools/input.js +26 -0
- package/build/src/tools/memory.js +29 -0
- package/build/src/tools/pages.js +7 -1
- package/build/src/tools/screencast.js +79 -0
- package/build/src/tools/slim/tools.js +81 -0
- package/build/src/tools/snapshot.js +5 -2
- package/build/src/tools/tools.js +35 -16
- package/build/src/version.js +9 -0
- package/package.json +6 -6
- package/build/src/third_party/issue-descriptions/corsInsecurePrivateNetwork.md +0 -10
- package/build/src/third_party/issue-descriptions/corsPreflightAllowPrivateNetworkError.md +0 -10
- package/build/src/third_party/issue-descriptions/corsPrivateNetworkPermissionDenied.md +0 -10
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Google LLC
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import { createServer } from 'node:net';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
11
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
12
|
+
import { logger } from '../logger.js';
|
|
13
|
+
import { PipeTransport } from '../third_party/index.js';
|
|
14
|
+
import { VERSION } from '../version.js';
|
|
15
|
+
import { getSocketPath, handlePidFile, INDEX_SCRIPT_PATH, IS_WINDOWS, } from './utils.js';
|
|
16
|
+
const pidFile = handlePidFile();
|
|
17
|
+
const socketPath = getSocketPath();
|
|
18
|
+
let mcpClient = null;
|
|
19
|
+
let mcpTransport = null;
|
|
20
|
+
let server = null;
|
|
21
|
+
async function setupMCPClient() {
|
|
22
|
+
console.log('Setting up MCP client connection...');
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
// Create stdio transport for chrome-devtools-mcp
|
|
25
|
+
mcpTransport = new StdioClientTransport({
|
|
26
|
+
command: process.execPath,
|
|
27
|
+
args: [INDEX_SCRIPT_PATH, ...args],
|
|
28
|
+
env: process.env,
|
|
29
|
+
});
|
|
30
|
+
mcpClient = new Client({
|
|
31
|
+
name: 'chrome-devtools-cli-daemon',
|
|
32
|
+
version: VERSION,
|
|
33
|
+
}, {
|
|
34
|
+
capabilities: {},
|
|
35
|
+
});
|
|
36
|
+
await mcpClient.connect(mcpTransport);
|
|
37
|
+
console.log('MCP client connected');
|
|
38
|
+
}
|
|
39
|
+
async function handleRequest(msg) {
|
|
40
|
+
try {
|
|
41
|
+
if (msg.method === 'invoke_tool') {
|
|
42
|
+
if (!mcpClient) {
|
|
43
|
+
throw new Error('MCP client not initialized');
|
|
44
|
+
}
|
|
45
|
+
const { tool, args } = msg;
|
|
46
|
+
const result = (await mcpClient.callTool({
|
|
47
|
+
name: tool,
|
|
48
|
+
arguments: args || {},
|
|
49
|
+
}));
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
result: JSON.stringify(result),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else if (msg.method === 'stop') {
|
|
56
|
+
// Trigger cleanup asynchronously
|
|
57
|
+
setImmediate(() => {
|
|
58
|
+
void cleanup();
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
message: 'stopping',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: `Unknown method: ${JSON.stringify(msg, null, 2)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: errorMessage,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function startSocketServer() {
|
|
81
|
+
// Remove existing socket file if it exists (only on non-Windows)
|
|
82
|
+
if (!IS_WINDOWS) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.unlink(socketPath);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// ignore errors.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return await new Promise((resolve, reject) => {
|
|
91
|
+
server = createServer(socket => {
|
|
92
|
+
const transport = new PipeTransport(socket, socket);
|
|
93
|
+
transport.onmessage = async (message) => {
|
|
94
|
+
logger('onmessage', message);
|
|
95
|
+
const response = await handleRequest(JSON.parse(message));
|
|
96
|
+
transport.send(JSON.stringify(response));
|
|
97
|
+
socket.end();
|
|
98
|
+
};
|
|
99
|
+
socket.on('error', error => {
|
|
100
|
+
logger('Socket error:', error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
server.listen({
|
|
104
|
+
path: socketPath,
|
|
105
|
+
readableAll: false,
|
|
106
|
+
writableAll: false,
|
|
107
|
+
}, async () => {
|
|
108
|
+
console.log(`Daemon server listening on ${socketPath}`);
|
|
109
|
+
try {
|
|
110
|
+
// Setup MCP client
|
|
111
|
+
await setupMCPClient();
|
|
112
|
+
resolve();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
server.on('error', error => {
|
|
119
|
+
logger('Server error:', error);
|
|
120
|
+
reject(error);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function cleanup() {
|
|
125
|
+
console.log('Cleaning up daemon...');
|
|
126
|
+
try {
|
|
127
|
+
await mcpClient?.close();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
logger('Error closing MCP client:', error);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await mcpTransport?.close();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
logger('Error closing MCP transport:', error);
|
|
137
|
+
}
|
|
138
|
+
server?.close(() => {
|
|
139
|
+
if (!IS_WINDOWS) {
|
|
140
|
+
void fs.unlink(socketPath).catch(() => undefined);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
await fs.unlink(pidFile).catch(() => undefined);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
// Handle shutdown signals
|
|
147
|
+
process.on('SIGTERM', () => {
|
|
148
|
+
void cleanup();
|
|
149
|
+
});
|
|
150
|
+
process.on('SIGINT', () => {
|
|
151
|
+
void cleanup();
|
|
152
|
+
});
|
|
153
|
+
process.on('SIGHUP', () => {
|
|
154
|
+
void cleanup();
|
|
155
|
+
});
|
|
156
|
+
// Handle uncaught errors
|
|
157
|
+
process.on('uncaughtException', error => {
|
|
158
|
+
logger('Uncaught exception:', error);
|
|
159
|
+
});
|
|
160
|
+
process.on('unhandledRejection', error => {
|
|
161
|
+
logger('Unhandled rejection:', error);
|
|
162
|
+
});
|
|
163
|
+
// Start the server
|
|
164
|
+
startSocketServer().catch(error => {
|
|
165
|
+
logger('Failed to start daemon server:', error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
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
|
+
const APP_NAME = 'chrome-devtools-mcp';
|
|
13
|
+
// Using these paths due to strict limits on the POSIX socket path length.
|
|
14
|
+
export function getSocketPath() {
|
|
15
|
+
const uid = os.userInfo().uid;
|
|
16
|
+
if (IS_WINDOWS) {
|
|
17
|
+
// Windows uses Named Pipes, not file paths.
|
|
18
|
+
// This format is required for server.listen()
|
|
19
|
+
return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
|
|
20
|
+
}
|
|
21
|
+
// 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
|
|
22
|
+
if (process.env.XDG_RUNTIME_DIR) {
|
|
23
|
+
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
|
|
24
|
+
}
|
|
25
|
+
// 2. macOS/Unix Fallback: Use /tmp/
|
|
26
|
+
// We use /tmp/ because it is much shorter than ~/Library/Application Support/
|
|
27
|
+
// and keeps us well under the 104-character limit.
|
|
28
|
+
return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
|
|
29
|
+
}
|
|
30
|
+
export function getRuntimeHome() {
|
|
31
|
+
const platform = os.platform();
|
|
32
|
+
const uid = os.userInfo().uid;
|
|
33
|
+
// 1. Check for the modern Unix standard
|
|
34
|
+
if (process.env.XDG_RUNTIME_DIR) {
|
|
35
|
+
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
|
|
36
|
+
}
|
|
37
|
+
// 2. Fallback for macOS and older Linux
|
|
38
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
39
|
+
// /tmp is cleared on boot, making it perfect for PIDs
|
|
40
|
+
return path.join('/tmp', `${APP_NAME}-${uid}`);
|
|
41
|
+
}
|
|
42
|
+
// 3. Windows Fallback
|
|
43
|
+
return path.join(os.tmpdir(), APP_NAME);
|
|
44
|
+
}
|
|
45
|
+
export const IS_WINDOWS = os.platform() === 'win32';
|
|
46
|
+
export function handlePidFile() {
|
|
47
|
+
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);
|
|
51
|
+
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);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Process is dead, we can safely overwrite the PID file
|
|
59
|
+
fs.unlinkSync(pidPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
fs.mkdirSync(path.dirname(pidPath), {
|
|
63
|
+
recursive: true,
|
|
64
|
+
});
|
|
65
|
+
fs.writeFileSync(pidPath, process.pid.toString());
|
|
66
|
+
return pidPath;
|
|
67
|
+
}
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
* Copyright 2026 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
var _a;
|
|
7
6
|
import { createStackTraceForConsoleMessage, SymbolizedError, } from '../DevtoolsUtils.js';
|
|
8
7
|
import { UncaughtError } from '../PageCollector.js';
|
|
9
8
|
import * as DevTools from '../third_party/index.js';
|
|
10
9
|
export class ConsoleFormatter {
|
|
11
|
-
static #STACK_TRACE_MAX_LINES = 50;
|
|
12
10
|
#id;
|
|
13
11
|
#type;
|
|
14
12
|
#text;
|
|
@@ -16,7 +14,7 @@ export class ConsoleFormatter {
|
|
|
16
14
|
#resolvedArgs;
|
|
17
15
|
#stack;
|
|
18
16
|
#cause;
|
|
19
|
-
|
|
17
|
+
isIgnored;
|
|
20
18
|
constructor(params) {
|
|
21
19
|
this.#id = params.id;
|
|
22
20
|
this.#type = params.type;
|
|
@@ -25,7 +23,7 @@ export class ConsoleFormatter {
|
|
|
25
23
|
this.#resolvedArgs = params.resolvedArgs ?? [];
|
|
26
24
|
this.#stack = params.stack;
|
|
27
25
|
this.#cause = params.cause;
|
|
28
|
-
this
|
|
26
|
+
this.isIgnored = params.isIgnored;
|
|
29
27
|
}
|
|
30
28
|
static async from(msg, options) {
|
|
31
29
|
const ignoreListManager = options?.devTools?.universe.context.get(DevTools.DevTools.IgnoreListManager);
|
|
@@ -51,7 +49,7 @@ export class ConsoleFormatter {
|
|
|
51
49
|
resolvedStackTraceForTesting: options?.resolvedStackTraceForTesting,
|
|
52
50
|
resolvedCauseForTesting: options?.resolvedCauseForTesting,
|
|
53
51
|
});
|
|
54
|
-
return new
|
|
52
|
+
return new ConsoleFormatter({
|
|
55
53
|
id: options.id,
|
|
56
54
|
type: 'error',
|
|
57
55
|
text: error.message,
|
|
@@ -96,7 +94,7 @@ export class ConsoleFormatter {
|
|
|
96
94
|
// ignore
|
|
97
95
|
}
|
|
98
96
|
}
|
|
99
|
-
return new
|
|
97
|
+
return new ConsoleFormatter({
|
|
100
98
|
id: options.id,
|
|
101
99
|
type: msg.type(),
|
|
102
100
|
text: msg.text(),
|
|
@@ -108,19 +106,11 @@ export class ConsoleFormatter {
|
|
|
108
106
|
}
|
|
109
107
|
// The short format for a console message.
|
|
110
108
|
toString() {
|
|
111
|
-
return
|
|
109
|
+
return convertConsoleMessageConciseToString(this.toJSON());
|
|
112
110
|
}
|
|
113
111
|
// The verbose format for a console message, including all details.
|
|
114
112
|
toStringDetailed() {
|
|
115
|
-
|
|
116
|
-
`ID: ${this.#id}`,
|
|
117
|
-
`Message: ${this.#type}> ${this.#text}`,
|
|
118
|
-
this.#formatArgs(),
|
|
119
|
-
this.#formatStackTrace(this.#stack, this.#cause, {
|
|
120
|
-
includeHeading: true,
|
|
121
|
-
}),
|
|
122
|
-
].filter(line => !!line);
|
|
123
|
-
return result.join('\n');
|
|
113
|
+
return convertConsoleMessageConciseDetailedToString(this.toJSONDetailed());
|
|
124
114
|
}
|
|
125
115
|
#getArgs() {
|
|
126
116
|
if (this.#resolvedArgs.length > 0) {
|
|
@@ -133,92 +123,6 @@ export class ConsoleFormatter {
|
|
|
133
123
|
}
|
|
134
124
|
return [];
|
|
135
125
|
}
|
|
136
|
-
#formatArg(arg) {
|
|
137
|
-
if (arg instanceof SymbolizedError) {
|
|
138
|
-
return [
|
|
139
|
-
arg.message,
|
|
140
|
-
this.#formatStackTrace(arg.stackTrace, arg.cause, {
|
|
141
|
-
includeHeading: false,
|
|
142
|
-
}),
|
|
143
|
-
]
|
|
144
|
-
.filter(line => !!line)
|
|
145
|
-
.join('\n');
|
|
146
|
-
}
|
|
147
|
-
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
|
|
148
|
-
}
|
|
149
|
-
#formatArgs() {
|
|
150
|
-
const args = this.#getArgs();
|
|
151
|
-
if (!args.length) {
|
|
152
|
-
return '';
|
|
153
|
-
}
|
|
154
|
-
const result = ['### Arguments'];
|
|
155
|
-
for (const [key, arg] of args.entries()) {
|
|
156
|
-
result.push(`Arg #${key}: ${this.#formatArg(arg)}`);
|
|
157
|
-
}
|
|
158
|
-
return result.join('\n');
|
|
159
|
-
}
|
|
160
|
-
#formatStackTrace(stackTrace, cause, opts) {
|
|
161
|
-
if (!stackTrace) {
|
|
162
|
-
return '';
|
|
163
|
-
}
|
|
164
|
-
const lines = this.#formatStackTraceInner(stackTrace, cause);
|
|
165
|
-
const includedLines = lines.slice(0, _a.#STACK_TRACE_MAX_LINES);
|
|
166
|
-
const reminderCount = lines.length - includedLines.length;
|
|
167
|
-
return [
|
|
168
|
-
opts.includeHeading ? '### Stack trace' : '',
|
|
169
|
-
...includedLines,
|
|
170
|
-
reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
|
|
171
|
-
'Note: line and column numbers use 1-based indexing',
|
|
172
|
-
]
|
|
173
|
-
.filter(line => !!line)
|
|
174
|
-
.join('\n');
|
|
175
|
-
}
|
|
176
|
-
#formatStackTraceInner(stackTrace, cause) {
|
|
177
|
-
if (!stackTrace) {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
return [
|
|
181
|
-
...this.#formatFragment(stackTrace.syncFragment),
|
|
182
|
-
...stackTrace.asyncFragments
|
|
183
|
-
.map(this.#formatAsyncFragment.bind(this))
|
|
184
|
-
.flat(),
|
|
185
|
-
...this.#formatCause(cause),
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
#formatFragment(fragment) {
|
|
189
|
-
const frames = fragment.frames.filter(frame => !this.#isIgnored(frame));
|
|
190
|
-
return frames.map(this.#formatFrame.bind(this));
|
|
191
|
-
}
|
|
192
|
-
#formatAsyncFragment(fragment) {
|
|
193
|
-
const formattedFrames = this.#formatFragment(fragment);
|
|
194
|
-
if (formattedFrames.length === 0) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
const separatorLineLength = 40;
|
|
198
|
-
const prefix = `--- ${fragment.description || 'async'} `;
|
|
199
|
-
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
|
|
200
|
-
return [separator, ...formattedFrames];
|
|
201
|
-
}
|
|
202
|
-
#formatFrame(frame) {
|
|
203
|
-
let result = `at ${frame.name ?? '<anonymous>'}`;
|
|
204
|
-
if (frame.uiSourceCode) {
|
|
205
|
-
const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
|
|
206
|
-
result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
|
|
207
|
-
}
|
|
208
|
-
else if (frame.url) {
|
|
209
|
-
result += ` (${frame.url}:${frame.line}:${frame.column})`;
|
|
210
|
-
}
|
|
211
|
-
return result;
|
|
212
|
-
}
|
|
213
|
-
#formatCause(cause) {
|
|
214
|
-
if (!cause) {
|
|
215
|
-
return [];
|
|
216
|
-
}
|
|
217
|
-
return [
|
|
218
|
-
`Caused by: ${cause.message}`,
|
|
219
|
-
...this.#formatStackTraceInner(cause.stackTrace, cause.cause),
|
|
220
|
-
];
|
|
221
|
-
}
|
|
222
126
|
toJSON() {
|
|
223
127
|
return {
|
|
224
128
|
type: this.#type,
|
|
@@ -232,9 +136,106 @@ export class ConsoleFormatter {
|
|
|
232
136
|
id: this.#id,
|
|
233
137
|
type: this.#type,
|
|
234
138
|
text: this.#text,
|
|
235
|
-
|
|
236
|
-
|
|
139
|
+
argsCount: this.#argCount,
|
|
140
|
+
args: this.#getArgs().map(arg => formatArg(arg, this)),
|
|
141
|
+
stackTrace: this.#stack
|
|
142
|
+
? formatStackTrace(this.#stack, this.#cause, this)
|
|
143
|
+
: undefined,
|
|
237
144
|
};
|
|
238
145
|
}
|
|
239
146
|
}
|
|
240
|
-
|
|
147
|
+
function convertConsoleMessageConciseToString(msg) {
|
|
148
|
+
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
|
|
149
|
+
}
|
|
150
|
+
function convertConsoleMessageConciseDetailedToString(msg) {
|
|
151
|
+
const result = [
|
|
152
|
+
`ID: ${msg.id}`,
|
|
153
|
+
`Message: ${msg.type}> ${msg.text}`,
|
|
154
|
+
formatArgs(msg),
|
|
155
|
+
...(msg.stackTrace ? ['### Stack trace', msg.stackTrace] : []),
|
|
156
|
+
].filter(line => !!line);
|
|
157
|
+
return result.join('\n');
|
|
158
|
+
}
|
|
159
|
+
function formatArgs(msg) {
|
|
160
|
+
const args = msg.args;
|
|
161
|
+
if (!args.length) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
const result = ['### Arguments'];
|
|
165
|
+
for (const [key, arg] of args.entries()) {
|
|
166
|
+
result.push(`Arg #${key}: ${arg}`);
|
|
167
|
+
}
|
|
168
|
+
return result.join('\n');
|
|
169
|
+
}
|
|
170
|
+
function formatArg(arg, formatter) {
|
|
171
|
+
if (arg instanceof SymbolizedError) {
|
|
172
|
+
return [
|
|
173
|
+
arg.message,
|
|
174
|
+
arg.stackTrace
|
|
175
|
+
? formatStackTrace(arg.stackTrace, arg.cause, formatter)
|
|
176
|
+
: undefined,
|
|
177
|
+
]
|
|
178
|
+
.filter(line => !!line)
|
|
179
|
+
.join('\n');
|
|
180
|
+
}
|
|
181
|
+
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
|
|
182
|
+
}
|
|
183
|
+
const STACK_TRACE_MAX_LINES = 50;
|
|
184
|
+
function formatStackTrace(stackTrace, cause, formatter) {
|
|
185
|
+
const lines = formatStackTraceInner(stackTrace, cause, formatter);
|
|
186
|
+
const includedLines = lines.slice(0, STACK_TRACE_MAX_LINES);
|
|
187
|
+
const reminderCount = lines.length - includedLines.length;
|
|
188
|
+
return [
|
|
189
|
+
...includedLines,
|
|
190
|
+
reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
|
|
191
|
+
'Note: line and column numbers use 1-based indexing',
|
|
192
|
+
]
|
|
193
|
+
.filter(line => !!line)
|
|
194
|
+
.join('\n');
|
|
195
|
+
}
|
|
196
|
+
function formatStackTraceInner(stackTrace, cause, formatter) {
|
|
197
|
+
if (!stackTrace) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
return [
|
|
201
|
+
...formatFragment(stackTrace.syncFragment, formatter),
|
|
202
|
+
...stackTrace.asyncFragments
|
|
203
|
+
.map(item => formatAsyncFragment(item, formatter))
|
|
204
|
+
.flat(),
|
|
205
|
+
...formatCause(cause, formatter),
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
function formatFragment(fragment, formatter) {
|
|
209
|
+
const frames = fragment.frames.filter(frame => !formatter.isIgnored(frame));
|
|
210
|
+
return frames.map(formatFrame);
|
|
211
|
+
}
|
|
212
|
+
function formatAsyncFragment(fragment, formatter) {
|
|
213
|
+
const formattedFrames = formatFragment(fragment, formatter);
|
|
214
|
+
if (formattedFrames.length === 0) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const separatorLineLength = 40;
|
|
218
|
+
const prefix = `--- ${fragment.description || 'async'} `;
|
|
219
|
+
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
|
|
220
|
+
return [separator, ...formattedFrames];
|
|
221
|
+
}
|
|
222
|
+
function formatFrame(frame) {
|
|
223
|
+
let result = `at ${frame.name ?? '<anonymous>'}`;
|
|
224
|
+
if (frame.uiSourceCode) {
|
|
225
|
+
const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
|
|
226
|
+
result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
|
|
227
|
+
}
|
|
228
|
+
else if (frame.url) {
|
|
229
|
+
result += ` (${frame.url}:${frame.line}:${frame.column})`;
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function formatCause(cause, formatter) {
|
|
234
|
+
if (!cause) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
return [
|
|
238
|
+
`Caused by: ${cause.message}`,
|
|
239
|
+
...formatStackTraceInner(cause.stackTrace, cause.cause, formatter),
|
|
240
|
+
];
|
|
241
|
+
}
|
|
@@ -14,56 +14,10 @@ export class IssueFormatter {
|
|
|
14
14
|
this.#options = options;
|
|
15
15
|
}
|
|
16
16
|
toString() {
|
|
17
|
-
|
|
18
|
-
const count = this.#issue.getAggregatedIssuesCount();
|
|
19
|
-
const idPart = this.#options.id !== undefined ? `msgid=${this.#options.id} ` : '';
|
|
20
|
-
return `${idPart}[issue] ${title} (count: ${count})`;
|
|
17
|
+
return convertIssueConciseToString(this.toJSON());
|
|
21
18
|
}
|
|
22
19
|
toStringDetailed() {
|
|
23
|
-
|
|
24
|
-
if (this.#options.id !== undefined) {
|
|
25
|
-
result.push(`ID: ${this.#options.id}`);
|
|
26
|
-
}
|
|
27
|
-
const bodyParts = [];
|
|
28
|
-
const description = this.#getDescription();
|
|
29
|
-
let processedMarkdown = description?.trim();
|
|
30
|
-
// Remove heading in order not to conflict with the whole console message response markdown
|
|
31
|
-
if (processedMarkdown?.startsWith('# ')) {
|
|
32
|
-
processedMarkdown = processedMarkdown.substring(2).trimStart();
|
|
33
|
-
}
|
|
34
|
-
if (processedMarkdown) {
|
|
35
|
-
bodyParts.push(processedMarkdown);
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
bodyParts.push(this.#getTitle() ?? 'Unknown Issue');
|
|
39
|
-
}
|
|
40
|
-
const links = this.#issue.getDescription()?.links;
|
|
41
|
-
if (links && links.length > 0) {
|
|
42
|
-
bodyParts.push('Learn more:');
|
|
43
|
-
for (const link of links) {
|
|
44
|
-
bodyParts.push(`[${link.linkTitle}](${link.link})`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const affectedResources = this.#getAffectedResources();
|
|
48
|
-
if (affectedResources.length) {
|
|
49
|
-
bodyParts.push('### Affected resources');
|
|
50
|
-
bodyParts.push(...affectedResources.map(item => {
|
|
51
|
-
const details = [];
|
|
52
|
-
if (item.uid) {
|
|
53
|
-
details.push(`uid=${item.uid}`);
|
|
54
|
-
}
|
|
55
|
-
if (item.request) {
|
|
56
|
-
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
|
|
57
|
-
item.request);
|
|
58
|
-
}
|
|
59
|
-
if (item.data) {
|
|
60
|
-
details.push(`data=${JSON.stringify(item.data)}`);
|
|
61
|
-
}
|
|
62
|
-
return details.join(' ');
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
|
-
result.push(`Message: issue> ${bodyParts.join('\n')}`);
|
|
66
|
-
return result.join('\n');
|
|
20
|
+
return convertIssueDetailedToString(this.toJSONDetailed());
|
|
67
21
|
}
|
|
68
22
|
toJSON() {
|
|
69
23
|
return {
|
|
@@ -77,6 +31,7 @@ export class IssueFormatter {
|
|
|
77
31
|
return {
|
|
78
32
|
id: this.#options.id,
|
|
79
33
|
type: 'issue',
|
|
34
|
+
count: this.#issue.getAggregatedIssuesCount(),
|
|
80
35
|
title: this.#getTitle(),
|
|
81
36
|
description: this.#getDescription(),
|
|
82
37
|
links: this.#issue.getDescription()?.links,
|
|
@@ -188,3 +143,50 @@ export class IssueFormatter {
|
|
|
188
143
|
}
|
|
189
144
|
}
|
|
190
145
|
}
|
|
146
|
+
function convertIssueConciseToString(issue) {
|
|
147
|
+
return `msgid=${issue.id} [issue] ${issue.title} (count: ${issue.count})`;
|
|
148
|
+
}
|
|
149
|
+
function convertIssueDetailedToString(issue) {
|
|
150
|
+
const result = [];
|
|
151
|
+
result.push(`ID: ${issue.id}`);
|
|
152
|
+
const bodyParts = [];
|
|
153
|
+
const description = issue.description;
|
|
154
|
+
let processedMarkdown = description?.trim();
|
|
155
|
+
// Remove heading in order not to conflict with the whole console message response markdown
|
|
156
|
+
if (processedMarkdown?.startsWith('# ')) {
|
|
157
|
+
processedMarkdown = processedMarkdown.substring(2).trimStart();
|
|
158
|
+
}
|
|
159
|
+
if (processedMarkdown) {
|
|
160
|
+
bodyParts.push(processedMarkdown);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
bodyParts.push(issue.title ?? 'Unknown Issue');
|
|
164
|
+
}
|
|
165
|
+
const links = issue.links;
|
|
166
|
+
if (links && links.length > 0) {
|
|
167
|
+
bodyParts.push('Learn more:');
|
|
168
|
+
for (const link of links) {
|
|
169
|
+
bodyParts.push(`[${link.linkTitle}](${link.link})`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const affectedResources = issue.affectedResources;
|
|
173
|
+
if (affectedResources.length) {
|
|
174
|
+
bodyParts.push('### Affected resources');
|
|
175
|
+
bodyParts.push(...affectedResources.map(item => {
|
|
176
|
+
const details = [];
|
|
177
|
+
if (item.uid) {
|
|
178
|
+
details.push(`uid=${item.uid}`);
|
|
179
|
+
}
|
|
180
|
+
if (item.request) {
|
|
181
|
+
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
|
|
182
|
+
item.request);
|
|
183
|
+
}
|
|
184
|
+
if (item.data) {
|
|
185
|
+
details.push(`data=${JSON.stringify(item.data)}`);
|
|
186
|
+
}
|
|
187
|
+
return details.join(' ');
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
result.push(`Message: issue> ${bodyParts.join('\n')}`);
|
|
191
|
+
return result.join('\n');
|
|
192
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -12,16 +12,14 @@ import { logger, saveLogsToFile } from './logger.js';
|
|
|
12
12
|
import { McpContext } from './McpContext.js';
|
|
13
13
|
import { McpResponse } from './McpResponse.js';
|
|
14
14
|
import { Mutex } from './Mutex.js';
|
|
15
|
+
import { SlimMcpResponse } from './SlimMcpResponse.js';
|
|
15
16
|
import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
|
|
16
17
|
import { computeFlagUsage } from './telemetry/flagUtils.js';
|
|
17
18
|
import { bucketizeLatency } from './telemetry/metricUtils.js';
|
|
18
19
|
import { McpServer, StdioServerTransport, SetLevelRequestSchema, } from './third_party/index.js';
|
|
19
20
|
import { ToolCategory } from './tools/categories.js';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
// x-release-please-start-version
|
|
23
|
-
const VERSION = '0.17.3';
|
|
24
|
-
// x-release-please-end
|
|
21
|
+
import { createTools } from './tools/tools.js';
|
|
22
|
+
import { VERSION } from './version.js';
|
|
25
23
|
export const args = parseArguments(VERSION);
|
|
26
24
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
27
25
|
if (process.env['CI'] ||
|
|
@@ -39,9 +37,11 @@ if (args.usageStatistics) {
|
|
|
39
37
|
clearcutIncludePidHeader: args.clearcutIncludePidHeader,
|
|
40
38
|
});
|
|
41
39
|
}
|
|
42
|
-
process.
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
if (process.env['CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT'] !== 'true') {
|
|
41
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
42
|
+
logger('Unhandled promise rejection', promise, reason);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
45
|
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
|
46
46
|
const server = new McpServer({
|
|
47
47
|
name: 'chrome_devtools',
|
|
@@ -96,10 +96,10 @@ const logDisclaimers = () => {
|
|
|
96
96
|
console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
|
|
97
97
|
debug, and modify any data in the browser or DevTools.
|
|
98
98
|
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
|
|
99
|
-
if (args.performanceCrux) {
|
|
99
|
+
if (!args.slim && args.performanceCrux) {
|
|
100
100
|
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.`);
|
|
101
101
|
}
|
|
102
|
-
if (args.usageStatistics) {
|
|
102
|
+
if (!args.slim && args.usageStatistics) {
|
|
103
103
|
console.error(`
|
|
104
104
|
Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
|
|
105
105
|
For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics`);
|
|
@@ -131,6 +131,10 @@ function registerTool(tool) {
|
|
|
131
131
|
!args.experimentalInteropTools) {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
+
if (tool.annotations.conditions?.includes('screencast') &&
|
|
135
|
+
!args.experimentalScreencast) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
134
138
|
server.registerTool(tool.name, {
|
|
135
139
|
description: tool.description,
|
|
136
140
|
inputSchema: tool.schema,
|
|
@@ -144,7 +148,7 @@ function registerTool(tool) {
|
|
|
144
148
|
const context = await getContext();
|
|
145
149
|
logger(`${tool.name} context: resolved`);
|
|
146
150
|
await context.detectOpenDevToolsWindows();
|
|
147
|
-
const response = new McpResponse();
|
|
151
|
+
const response = args.slim ? new SlimMcpResponse() : new McpResponse();
|
|
148
152
|
await tool.handler({
|
|
149
153
|
params,
|
|
150
154
|
}, response, context);
|
|
@@ -184,6 +188,7 @@ function registerTool(tool) {
|
|
|
184
188
|
}
|
|
185
189
|
});
|
|
186
190
|
}
|
|
191
|
+
const tools = createTools(args);
|
|
187
192
|
for (const tool of tools) {
|
|
188
193
|
registerTool(tool);
|
|
189
194
|
}
|