chrome-devtools-mcp 0.25.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -6
- package/build/src/DevtoolsUtils.js +13 -0
- package/build/src/HeapSnapshotManager.js +26 -2
- package/build/src/McpContext.js +3 -0
- package/build/src/McpResponse.js +51 -21
- package/build/src/ToolHandler.js +217 -0
- package/build/src/WaitForHelper.js +18 -4
- package/build/src/bin/check-latest-version.js +25 -1
- package/build/src/bin/chrome-devtools-cli-options.js +38 -2
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
- package/build/src/bin/chrome-devtools-mcp-main.js +4 -3
- package/build/src/bin/chrome-devtools.js +0 -2
- package/build/src/daemon/client.js +12 -6
- package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
- package/build/src/index.js +11 -164
- package/build/src/telemetry/ClearcutLogger.js +34 -118
- package/build/src/telemetry/errors.js +18 -0
- package/build/src/telemetry/flagUtils.js +4 -3
- package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
- package/build/src/telemetry/persistence.js +20 -2
- package/build/src/telemetry/transformation.js +134 -0
- package/build/src/telemetry/types.js +0 -8
- package/build/src/third_party/THIRD_PARTY_NOTICES +140 -857
- package/build/src/third_party/bundled-packages.json +3 -3
- package/build/src/third_party/devtools-formatter-worker.js +475 -146
- package/build/src/third_party/devtools-heap-snapshot-worker.js +39 -44
- package/build/src/third_party/index.js +4055 -30401
- package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
- package/build/src/tools/ToolDefinition.js +1 -1
- package/build/src/tools/emulation.js +3 -2
- package/build/src/tools/input.js +46 -16
- package/build/src/tools/lighthouse.js +7 -7
- package/build/src/tools/memory.js +24 -0
- package/build/src/tools/script.js +32 -10
- package/build/src/version.js +1 -1
- package/package.json +10 -7
- package/build/src/telemetry/metricUtils.js +0 -15
|
@@ -114,7 +114,7 @@ export const commands = {
|
|
|
114
114
|
geolocation: {
|
|
115
115
|
name: 'geolocation',
|
|
116
116
|
type: 'string',
|
|
117
|
-
description: 'Geolocation (`<latitude
|
|
117
|
+
description: 'Geolocation (`<latitude>,<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.',
|
|
118
118
|
required: false,
|
|
119
119
|
},
|
|
120
120
|
userAgent: {
|
|
@@ -154,6 +154,12 @@ export const commands = {
|
|
|
154
154
|
description: 'An optional list of arguments to pass to the function.',
|
|
155
155
|
required: false,
|
|
156
156
|
},
|
|
157
|
+
filePath: {
|
|
158
|
+
name: 'filePath',
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.',
|
|
161
|
+
required: false,
|
|
162
|
+
},
|
|
157
163
|
dialogAction: {
|
|
158
164
|
name: 'dialogAction',
|
|
159
165
|
type: 'string',
|
|
@@ -211,7 +217,7 @@ export const commands = {
|
|
|
211
217
|
value: {
|
|
212
218
|
name: 'value',
|
|
213
219
|
type: 'string',
|
|
214
|
-
description: 'The value to fill in',
|
|
220
|
+
description: 'The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
|
|
215
221
|
required: true,
|
|
216
222
|
},
|
|
217
223
|
includeSnapshot: {
|
|
@@ -282,6 +288,36 @@ export const commands = {
|
|
|
282
288
|
},
|
|
283
289
|
},
|
|
284
290
|
},
|
|
291
|
+
get_node_retainers: {
|
|
292
|
+
description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --experimentalMemory=true)',
|
|
293
|
+
category: 'Memory',
|
|
294
|
+
args: {
|
|
295
|
+
filePath: {
|
|
296
|
+
name: 'filePath',
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'A path to a .heapsnapshot file to read.',
|
|
299
|
+
required: true,
|
|
300
|
+
},
|
|
301
|
+
nodeId: {
|
|
302
|
+
name: 'nodeId',
|
|
303
|
+
type: 'number',
|
|
304
|
+
description: 'The stable node ID to get retainers for.',
|
|
305
|
+
required: true,
|
|
306
|
+
},
|
|
307
|
+
pageIdx: {
|
|
308
|
+
name: 'pageIdx',
|
|
309
|
+
type: 'number',
|
|
310
|
+
description: 'The page index for pagination.',
|
|
311
|
+
required: false,
|
|
312
|
+
},
|
|
313
|
+
pageSize: {
|
|
314
|
+
name: 'pageSize',
|
|
315
|
+
type: 'number',
|
|
316
|
+
description: 'The page size for pagination.',
|
|
317
|
+
required: false,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
285
321
|
get_nodes_by_class: {
|
|
286
322
|
description: 'Loads a memory heapsnapshot and returns instances of a specific class with their stable IDs. (requires flag: --experimentalMemory=true)',
|
|
287
323
|
category: 'Memory',
|
|
@@ -136,33 +136,27 @@ export const cliOptions = {
|
|
|
136
136
|
},
|
|
137
137
|
experimentalPageIdRouting: {
|
|
138
138
|
type: 'boolean',
|
|
139
|
-
describe: 'Whether to expose pageId on page-scoped tools and route requests by page ID.',
|
|
140
|
-
hidden: true,
|
|
139
|
+
describe: 'Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).',
|
|
141
140
|
},
|
|
142
141
|
experimentalDevtools: {
|
|
143
142
|
type: 'boolean',
|
|
144
143
|
describe: 'Whether to enable automation over DevTools targets',
|
|
145
|
-
hidden: true,
|
|
146
144
|
},
|
|
147
145
|
experimentalVision: {
|
|
148
146
|
type: 'boolean',
|
|
149
147
|
describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
|
|
150
|
-
hidden: false,
|
|
151
148
|
},
|
|
152
149
|
experimentalMemory: {
|
|
153
150
|
type: 'boolean',
|
|
154
151
|
describe: 'Whether to enable experimental memory tools.',
|
|
155
|
-
hidden: true,
|
|
156
152
|
},
|
|
157
153
|
experimentalStructuredContent: {
|
|
158
154
|
type: 'boolean',
|
|
159
155
|
describe: 'Whether to output structured formatted content.',
|
|
160
|
-
hidden: true,
|
|
161
156
|
},
|
|
162
157
|
experimentalIncludeAllPages: {
|
|
163
158
|
type: 'boolean',
|
|
164
159
|
describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
|
|
165
|
-
hidden: true,
|
|
166
160
|
},
|
|
167
161
|
experimentalNavigationAllowlist: {
|
|
168
162
|
type: 'boolean',
|
|
@@ -257,7 +251,7 @@ export const cliOptions = {
|
|
|
257
251
|
},
|
|
258
252
|
redactNetworkHeaders: {
|
|
259
253
|
type: 'boolean',
|
|
260
|
-
describe: 'If true, redacts some of the network headers considered
|
|
254
|
+
describe: 'If true, redacts some of the network headers considered sensitive before returning to the client.',
|
|
261
255
|
default: false,
|
|
262
256
|
},
|
|
263
257
|
};
|
|
@@ -7,6 +7,7 @@ import '../polyfill.js';
|
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
import { createMcpServer, logDisclaimers } from '../index.js';
|
|
9
9
|
import { logger, saveLogsToFile } from '../logger.js';
|
|
10
|
+
import { ClearcutLogger } from '../telemetry/ClearcutLogger.js';
|
|
10
11
|
import { computeFlagUsage } from '../telemetry/flagUtils.js';
|
|
11
12
|
import { StdioServerTransport } from '../third_party/index.js';
|
|
12
13
|
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
@@ -21,13 +22,13 @@ if (process.env['CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT'] !== 'true') {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
|
24
|
-
const { server
|
|
25
|
+
const { server } = await createMcpServer(args, {
|
|
25
26
|
logFile,
|
|
26
27
|
});
|
|
27
28
|
const transport = new StdioServerTransport();
|
|
28
29
|
await server.connect(transport);
|
|
29
30
|
logger('Chrome DevTools MCP Server connected');
|
|
30
31
|
logDisclaimers(args);
|
|
31
|
-
void
|
|
32
|
-
void
|
|
32
|
+
void ClearcutLogger.get()?.logDailyActiveIfNeeded();
|
|
33
|
+
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, cliOptions));
|
|
33
34
|
//# sourceMappingURL=chrome-devtools-mcp-main.js.map
|
|
@@ -24,8 +24,6 @@ const defaultArgs = ['--viaCli', '--experimentalStructuredContent'];
|
|
|
24
24
|
const startCliOptions = {
|
|
25
25
|
...cliOptions,
|
|
26
26
|
};
|
|
27
|
-
// Not supported in CLI on purpose.
|
|
28
|
-
delete startCliOptions.autoConnect;
|
|
29
27
|
// Missing CLI serialization.
|
|
30
28
|
delete startCliOptions.viewport;
|
|
31
29
|
// Change the defaults for the CLI.
|
|
@@ -115,13 +115,8 @@ export async function handleResponse(response, format) {
|
|
|
115
115
|
if (response.isError) {
|
|
116
116
|
return JSON.stringify(response.content);
|
|
117
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
118
|
const chunks = [];
|
|
119
|
+
const images = [];
|
|
125
120
|
for (const content of response.content) {
|
|
126
121
|
if (content.type === 'text') {
|
|
127
122
|
chunks.push(content.text);
|
|
@@ -143,12 +138,23 @@ export async function handleResponse(response, format) {
|
|
|
143
138
|
const name = crypto.randomUUID();
|
|
144
139
|
const filepath = await getTempFilePath(`${name}${extension}`);
|
|
145
140
|
fs.writeFileSync(filepath, data);
|
|
141
|
+
images.push({ filePath: filepath, mimeType });
|
|
146
142
|
chunks.push(`Saved to ${filepath}.`);
|
|
147
143
|
}
|
|
148
144
|
else {
|
|
149
145
|
throw new Error('Not supported response content type');
|
|
150
146
|
}
|
|
151
147
|
}
|
|
148
|
+
if (format === 'json') {
|
|
149
|
+
if (response.structuredContent) {
|
|
150
|
+
const structuredContent = {
|
|
151
|
+
...response.structuredContent,
|
|
152
|
+
...(images.length ? { images } : {}),
|
|
153
|
+
};
|
|
154
|
+
return JSON.stringify(structuredContent);
|
|
155
|
+
}
|
|
156
|
+
// Fall-through to text for backward compatibility.
|
|
157
|
+
}
|
|
152
158
|
return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
|
|
153
159
|
}
|
|
154
160
|
//# sourceMappingURL=client.js.map
|
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
* Copyright 2026 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { DevTools } from '../third_party/index.js';
|
|
6
7
|
import { stableIdSymbol } from '../utils/id.js';
|
|
7
8
|
export function isNodeLike(item) {
|
|
8
9
|
return (typeof item === 'object' && item !== null && 'id' in item && 'name' in item);
|
|
9
10
|
}
|
|
11
|
+
export function isEdgeLike(item) {
|
|
12
|
+
return (typeof item === 'object' &&
|
|
13
|
+
item !== null &&
|
|
14
|
+
'name' in item &&
|
|
15
|
+
'node' in item &&
|
|
16
|
+
'type' in item &&
|
|
17
|
+
typeof item.node === 'object' &&
|
|
18
|
+
item.node !== null &&
|
|
19
|
+
'id' in item.node &&
|
|
20
|
+
'name' in item.node);
|
|
21
|
+
}
|
|
10
22
|
export class HeapSnapshotFormatter {
|
|
11
23
|
#aggregates;
|
|
12
24
|
constructor(aggregates) {
|
|
@@ -14,12 +26,21 @@ export class HeapSnapshotFormatter {
|
|
|
14
26
|
}
|
|
15
27
|
static formatNodes(items) {
|
|
16
28
|
const lines = [];
|
|
17
|
-
if (items.length > 0
|
|
18
|
-
|
|
29
|
+
if (items.length > 0) {
|
|
30
|
+
const firstItem = items[0];
|
|
31
|
+
if (isNodeLike(firstItem)) {
|
|
32
|
+
lines.push('id,name,type,distance,selfSize,retainedSize');
|
|
33
|
+
}
|
|
34
|
+
else if (isEdgeLike(firstItem)) {
|
|
35
|
+
lines.push('edgeIndex,edgeName,edgeType,targetNodeId,targetNodeName');
|
|
36
|
+
}
|
|
19
37
|
}
|
|
20
38
|
for (const item of items) {
|
|
21
39
|
if (isNodeLike(item)) {
|
|
22
|
-
lines.push(`${item.id}
|
|
40
|
+
lines.push(`${item.id},${item.name},${item.type},${item.distance},${DevTools.I18n.ByteUtilities.formatBytesToKb(item.selfSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(item.retainedSize)}`);
|
|
41
|
+
}
|
|
42
|
+
else if (isEdgeLike(item)) {
|
|
43
|
+
lines.push(`${item.edgeIndex},${item.name},${item.type},${item.node.id},${item.node.name}`);
|
|
23
44
|
}
|
|
24
45
|
}
|
|
25
46
|
return lines.join('\n');
|
|
@@ -33,7 +54,7 @@ export class HeapSnapshotFormatter {
|
|
|
33
54
|
lines.push('uid,className,count,selfSize,maxRetainedSize');
|
|
34
55
|
for (const info of sorted) {
|
|
35
56
|
const uid = info[stableIdSymbol] ?? '';
|
|
36
|
-
lines.push(`${uid}
|
|
57
|
+
lines.push(`${uid},${info.name},${info.count},${DevTools.I18n.ByteUtilities.formatBytesToKb(info.self)},${DevTools.I18n.ByteUtilities.formatBytesToKb(info.maxRet)}`);
|
|
37
58
|
}
|
|
38
59
|
return lines.join('\n');
|
|
39
60
|
}
|
|
@@ -43,8 +64,8 @@ export class HeapSnapshotFormatter {
|
|
|
43
64
|
uid: info[stableIdSymbol],
|
|
44
65
|
className: info.name,
|
|
45
66
|
count: info.count,
|
|
46
|
-
selfSize: info.self,
|
|
47
|
-
retainedSize: info.maxRet,
|
|
67
|
+
selfSize: DevTools.I18n.ByteUtilities.formatBytesToKb(info.self),
|
|
68
|
+
retainedSize: DevTools.I18n.ByteUtilities.formatBytesToKb(info.maxRet),
|
|
48
69
|
}));
|
|
49
70
|
}
|
|
50
71
|
static sort(aggregates) {
|
package/build/src/index.js
CHANGED
|
@@ -7,77 +7,18 @@ import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
|
|
|
7
7
|
import { loadIssueDescriptions } from './issue-descriptions.js';
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
9
|
import { McpContext } from './McpContext.js';
|
|
10
|
-
import { McpResponse } from './McpResponse.js';
|
|
11
10
|
import { Mutex } from './Mutex.js';
|
|
12
|
-
import { SlimMcpResponse } from './SlimMcpResponse.js';
|
|
13
11
|
import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
|
|
14
|
-
import {
|
|
12
|
+
import { FilePersistence } from './telemetry/persistence.js';
|
|
15
13
|
import { McpServer, SetLevelRequestSchema, ListRootsResultSchema, RootsListChangedNotificationSchema, } from './third_party/index.js';
|
|
16
|
-
import {
|
|
17
|
-
import { pageIdSchema } from './tools/ToolDefinition.js';
|
|
14
|
+
import { ToolHandler } from './ToolHandler.js';
|
|
18
15
|
import { createTools } from './tools/tools.js';
|
|
19
16
|
import { VERSION } from './version.js';
|
|
20
|
-
export
|
|
21
|
-
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
|
22
|
-
}
|
|
23
|
-
function buildDisabledMessage(toolName, flag, categoryLabel) {
|
|
24
|
-
const reason = categoryLabel
|
|
25
|
-
? `is in category ${categoryLabel} which`
|
|
26
|
-
: `requires experimental feature ${flag} and`;
|
|
27
|
-
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
|
|
28
|
-
}
|
|
29
|
-
function getCategoryStatus(category, serverArgs) {
|
|
30
|
-
const categoryFlag = buildFlag(category);
|
|
31
|
-
const flagValue = serverArgs[categoryFlag];
|
|
32
|
-
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
|
|
33
|
-
? !flagValue
|
|
34
|
-
: flagValue === false;
|
|
35
|
-
if (isDisabled) {
|
|
36
|
-
return {
|
|
37
|
-
categoryFlag,
|
|
38
|
-
disabled: true,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
disabled: false,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function getConditionStatus(condition, serverArgs) {
|
|
46
|
-
if (condition && !serverArgs[condition]) {
|
|
47
|
-
return { conditionFlag: condition, disabled: true };
|
|
48
|
-
}
|
|
49
|
-
return { disabled: false };
|
|
50
|
-
}
|
|
51
|
-
function getToolStatusInfo(tool, serverArgs) {
|
|
52
|
-
const category = tool.annotations.category;
|
|
53
|
-
const categoryCheck = getCategoryStatus(category, serverArgs);
|
|
54
|
-
if (category && categoryCheck.disabled) {
|
|
55
|
-
if (!categoryCheck.categoryFlag) {
|
|
56
|
-
throw new Error('when the category is disabled there should always be a flag set');
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
disabled: true,
|
|
60
|
-
reason: buildDisabledMessage(tool.name, `--${categoryCheck.categoryFlag}`, labels[category]),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
for (const condition of tool.annotations.conditions || []) {
|
|
64
|
-
const conditionCheck = getConditionStatus(condition, serverArgs);
|
|
65
|
-
if (conditionCheck.disabled) {
|
|
66
|
-
if (!conditionCheck.conditionFlag) {
|
|
67
|
-
throw new Error('when the condition is disabled there should always be a flag set');
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
disabled: true,
|
|
71
|
-
reason: buildDisabledMessage(tool.name, `--${conditionCheck.conditionFlag}`),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { disabled: false };
|
|
76
|
-
}
|
|
17
|
+
export { buildFlag } from './ToolHandler.js';
|
|
77
18
|
export async function createMcpServer(serverArgs, options) {
|
|
78
|
-
let clearcutLogger;
|
|
79
19
|
if (serverArgs.usageStatistics) {
|
|
80
|
-
|
|
20
|
+
ClearcutLogger.initialize({
|
|
21
|
+
persistence: new FilePersistence(),
|
|
81
22
|
logFile: serverArgs.logFile,
|
|
82
23
|
appVersion: VERSION,
|
|
83
24
|
clearcutEndpoint: serverArgs.clearcutEndpoint,
|
|
@@ -108,7 +49,7 @@ export async function createMcpServer(serverArgs, options) {
|
|
|
108
49
|
server.server.oninitialized = () => {
|
|
109
50
|
const clientName = server.server.getClientVersion()?.name;
|
|
110
51
|
if (clientName) {
|
|
111
|
-
|
|
52
|
+
ClearcutLogger.get()?.setClientName(clientName);
|
|
112
53
|
}
|
|
113
54
|
if (server.server.getClientCapabilities()?.roots) {
|
|
114
55
|
void updateRoots();
|
|
@@ -164,110 +105,16 @@ export async function createMcpServer(serverArgs, options) {
|
|
|
164
105
|
}
|
|
165
106
|
const toolMutex = new Mutex();
|
|
166
107
|
function registerTool(tool) {
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
108
|
+
const toolHandler = new ToolHandler(tool, serverArgs, getContext, toolMutex);
|
|
109
|
+
if (!toolHandler.shouldRegister) {
|
|
169
110
|
return;
|
|
170
111
|
}
|
|
171
|
-
const schema = 'pageScoped' in tool &&
|
|
172
|
-
tool.pageScoped &&
|
|
173
|
-
serverArgs.experimentalPageIdRouting &&
|
|
174
|
-
!serverArgs.slim
|
|
175
|
-
? { ...tool.schema, ...pageIdSchema }
|
|
176
|
-
: tool.schema;
|
|
177
112
|
server.registerTool(tool.name, {
|
|
178
113
|
description: tool.description,
|
|
179
|
-
inputSchema:
|
|
114
|
+
inputSchema: toolHandler.registeredInputSchema,
|
|
180
115
|
annotations: tool.annotations,
|
|
181
116
|
}, async (params) => {
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
content: [
|
|
185
|
-
{
|
|
186
|
-
type: 'text',
|
|
187
|
-
text: disabledReason,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
isError: true,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
const guard = await toolMutex.acquire();
|
|
194
|
-
const startTime = Date.now();
|
|
195
|
-
let success = false;
|
|
196
|
-
try {
|
|
197
|
-
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
|
|
198
|
-
const context = await getContext();
|
|
199
|
-
logger(`${tool.name} context: resolved`);
|
|
200
|
-
await context.detectOpenDevToolsWindows();
|
|
201
|
-
const response = serverArgs.slim
|
|
202
|
-
? new SlimMcpResponse(serverArgs)
|
|
203
|
-
: new McpResponse(serverArgs);
|
|
204
|
-
response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
|
|
205
|
-
try {
|
|
206
|
-
const page = serverArgs.experimentalPageIdRouting &&
|
|
207
|
-
params.pageId &&
|
|
208
|
-
!serverArgs.slim
|
|
209
|
-
? context.getPageById(params.pageId)
|
|
210
|
-
: context.getSelectedMcpPage();
|
|
211
|
-
response.setPage(page);
|
|
212
|
-
if (tool.blockedByDialog) {
|
|
213
|
-
page.throwIfDialogOpen();
|
|
214
|
-
}
|
|
215
|
-
if ('pageScoped' in tool && tool.pageScoped) {
|
|
216
|
-
await tool.handler({
|
|
217
|
-
params,
|
|
218
|
-
page,
|
|
219
|
-
}, response, context);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
await tool.handler(
|
|
223
|
-
// @ts-expect-error types do not match.
|
|
224
|
-
{
|
|
225
|
-
params,
|
|
226
|
-
}, response, context);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
response.setError(err);
|
|
231
|
-
}
|
|
232
|
-
const { content, structuredContent } = await response.handle(tool.name, context);
|
|
233
|
-
const result = {
|
|
234
|
-
content,
|
|
235
|
-
};
|
|
236
|
-
if (response.error) {
|
|
237
|
-
result.isError = true;
|
|
238
|
-
}
|
|
239
|
-
success = true;
|
|
240
|
-
if (serverArgs.experimentalStructuredContent) {
|
|
241
|
-
result.structuredContent = structuredContent;
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
logger(`${tool.name} error:`, err, err?.stack);
|
|
247
|
-
let errorText = err && 'message' in err ? err.message : String(err);
|
|
248
|
-
if ('cause' in err && err.cause) {
|
|
249
|
-
errorText += `\nCause: ${err.cause.message}`;
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
content: [
|
|
253
|
-
{
|
|
254
|
-
type: 'text',
|
|
255
|
-
text: errorText,
|
|
256
|
-
},
|
|
257
|
-
],
|
|
258
|
-
isError: true,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
void clearcutLogger?.logToolInvocation({
|
|
263
|
-
toolName: tool.name,
|
|
264
|
-
params,
|
|
265
|
-
schema,
|
|
266
|
-
success,
|
|
267
|
-
latencyMs: bucketizeLatency(Date.now() - startTime),
|
|
268
|
-
});
|
|
269
|
-
guard.dispose();
|
|
270
|
-
}
|
|
117
|
+
return await toolHandler.handle(params);
|
|
271
118
|
});
|
|
272
119
|
}
|
|
273
120
|
const tools = createTools(serverArgs);
|
|
@@ -275,7 +122,7 @@ export async function createMcpServer(serverArgs, options) {
|
|
|
275
122
|
registerTool(tool);
|
|
276
123
|
}
|
|
277
124
|
await loadIssueDescriptions();
|
|
278
|
-
return { server
|
|
125
|
+
return { server };
|
|
279
126
|
}
|
|
280
127
|
export const logDisclaimers = (args) => {
|
|
281
128
|
console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
|
|
@@ -6,124 +6,10 @@
|
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { DAEMON_CLIENT_NAME } from '../daemon/utils.js';
|
|
8
8
|
import { logger } from '../logger.js';
|
|
9
|
-
import {
|
|
9
|
+
import { sanitizeParams, stripUnderscoreBeforeNumber } from './transformation.js';
|
|
10
10
|
import { McpClient, WatchdogMessageType, OsType, } from './types.js';
|
|
11
11
|
import { WatchdogClient } from './WatchdogClient.js';
|
|
12
12
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
13
|
-
export const PARAM_BLOCKLIST = new Set(['uid', 'reqid', 'msgid']);
|
|
14
|
-
const SUPPORTED_ZOD_TYPES = [
|
|
15
|
-
'ZodString',
|
|
16
|
-
'ZodNumber',
|
|
17
|
-
'ZodBoolean',
|
|
18
|
-
'ZodArray',
|
|
19
|
-
'ZodEnum',
|
|
20
|
-
];
|
|
21
|
-
function isZodType(type) {
|
|
22
|
-
return SUPPORTED_ZOD_TYPES.includes(type);
|
|
23
|
-
}
|
|
24
|
-
export function getZodType(zodType) {
|
|
25
|
-
const def = zodType._def;
|
|
26
|
-
const typeName = def.typeName;
|
|
27
|
-
if (typeName === 'ZodOptional' ||
|
|
28
|
-
typeName === 'ZodDefault' ||
|
|
29
|
-
typeName === 'ZodNullable') {
|
|
30
|
-
return getZodType(def.innerType);
|
|
31
|
-
}
|
|
32
|
-
if (typeName === 'ZodEffects') {
|
|
33
|
-
return getZodType(def.schema);
|
|
34
|
-
}
|
|
35
|
-
if (isZodType(typeName)) {
|
|
36
|
-
return typeName;
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
|
|
39
|
-
}
|
|
40
|
-
export function transformArgName(zodType, name) {
|
|
41
|
-
const snakeCaseName = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
42
|
-
if (zodType === 'ZodString') {
|
|
43
|
-
return `${snakeCaseName}_length`;
|
|
44
|
-
}
|
|
45
|
-
else if (zodType === 'ZodArray') {
|
|
46
|
-
return `${snakeCaseName}_count`;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
return snakeCaseName;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export function transformArgType(zodType) {
|
|
53
|
-
if (zodType === 'ZodString' || zodType === 'ZodArray') {
|
|
54
|
-
return 'number';
|
|
55
|
-
}
|
|
56
|
-
switch (zodType) {
|
|
57
|
-
case 'ZodNumber':
|
|
58
|
-
return 'number';
|
|
59
|
-
case 'ZodBoolean':
|
|
60
|
-
return 'boolean';
|
|
61
|
-
case 'ZodEnum':
|
|
62
|
-
return 'enum';
|
|
63
|
-
default:
|
|
64
|
-
throw new Error(`Unsupported zod type for tool parameter: ${zodType}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const BUCKETS = [
|
|
68
|
-
0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000,
|
|
69
|
-
];
|
|
70
|
-
function bucketize(value) {
|
|
71
|
-
for (const bucket of BUCKETS) {
|
|
72
|
-
if (bucket >= value) {
|
|
73
|
-
return bucket;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return BUCKETS[BUCKETS.length - 1];
|
|
77
|
-
}
|
|
78
|
-
function transformValue(zodType, value) {
|
|
79
|
-
if (zodType === 'ZodString') {
|
|
80
|
-
return bucketize(value.length);
|
|
81
|
-
}
|
|
82
|
-
else if (zodType === 'ZodArray') {
|
|
83
|
-
return value.length;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function hasEquivalentType(zodType, value) {
|
|
90
|
-
if (zodType === 'ZodString') {
|
|
91
|
-
return typeof value === 'string';
|
|
92
|
-
}
|
|
93
|
-
else if (zodType === 'ZodArray') {
|
|
94
|
-
return Array.isArray(value);
|
|
95
|
-
}
|
|
96
|
-
else if (zodType === 'ZodNumber') {
|
|
97
|
-
return typeof value === 'number';
|
|
98
|
-
}
|
|
99
|
-
else if (zodType === 'ZodBoolean') {
|
|
100
|
-
return typeof value === 'boolean';
|
|
101
|
-
}
|
|
102
|
-
else if (zodType === 'ZodEnum') {
|
|
103
|
-
return (typeof value === 'string' ||
|
|
104
|
-
typeof value === 'number' ||
|
|
105
|
-
typeof value === 'boolean');
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
export function sanitizeParams(params, schema) {
|
|
112
|
-
const transformed = {};
|
|
113
|
-
for (const [name, value] of Object.entries(params)) {
|
|
114
|
-
if (PARAM_BLOCKLIST.has(name)) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const zodType = getZodType(schema[name]);
|
|
118
|
-
if (!hasEquivalentType(zodType, value)) {
|
|
119
|
-
throw new Error(`parameter ${name} has type ${zodType} but value ${value} is not of equivalent type`);
|
|
120
|
-
}
|
|
121
|
-
const transformedName = transformArgName(zodType, name);
|
|
122
|
-
const transformedValue = transformValue(zodType, value);
|
|
123
|
-
transformed[transformedName] = transformedValue;
|
|
124
|
-
}
|
|
125
|
-
return transformed;
|
|
126
|
-
}
|
|
127
13
|
function detectOsType() {
|
|
128
14
|
switch (process.platform) {
|
|
129
15
|
case 'win32':
|
|
@@ -136,12 +22,27 @@ function detectOsType() {
|
|
|
136
22
|
return OsType.OS_TYPE_UNSPECIFIED;
|
|
137
23
|
}
|
|
138
24
|
}
|
|
25
|
+
// Not const to allow resetting the instance for testing purposes.
|
|
26
|
+
let _clearcut_logger_instance;
|
|
139
27
|
export class ClearcutLogger {
|
|
140
28
|
#persistence;
|
|
141
29
|
#watchdog;
|
|
142
30
|
#mcpClient;
|
|
31
|
+
static initialize(options) {
|
|
32
|
+
if (_clearcut_logger_instance) {
|
|
33
|
+
throw new Error('ClearcutLogger is already initialized');
|
|
34
|
+
}
|
|
35
|
+
_clearcut_logger_instance = new ClearcutLogger(options);
|
|
36
|
+
return _clearcut_logger_instance;
|
|
37
|
+
}
|
|
38
|
+
static get() {
|
|
39
|
+
return _clearcut_logger_instance;
|
|
40
|
+
}
|
|
41
|
+
static resetForTesting() {
|
|
42
|
+
_clearcut_logger_instance = undefined;
|
|
43
|
+
}
|
|
143
44
|
constructor(options) {
|
|
144
|
-
this.#persistence = options.persistence
|
|
45
|
+
this.#persistence = options.persistence;
|
|
145
46
|
this.#watchdog =
|
|
146
47
|
options.watchdogClient ??
|
|
147
48
|
new WatchdogClient({
|
|
@@ -180,14 +81,15 @@ export class ClearcutLogger {
|
|
|
180
81
|
}
|
|
181
82
|
}
|
|
182
83
|
async logToolInvocation(args) {
|
|
84
|
+
const sanitizedToolName = stripUnderscoreBeforeNumber(args.toolName);
|
|
183
85
|
const tool_invocation = {
|
|
184
|
-
tool_name:
|
|
86
|
+
tool_name: sanitizedToolName,
|
|
185
87
|
success: args.success,
|
|
186
88
|
latency_ms: args.latencyMs,
|
|
187
89
|
};
|
|
188
90
|
if (Object.keys(args.params).length > 0) {
|
|
189
91
|
tool_invocation.tool_params = {
|
|
190
|
-
[`${
|
|
92
|
+
[`${sanitizedToolName}_params`]: sanitizeParams(args.params, args.schema),
|
|
191
93
|
};
|
|
192
94
|
}
|
|
193
95
|
this.#watchdog.send({
|
|
@@ -237,6 +139,20 @@ export class ClearcutLogger {
|
|
|
237
139
|
logger('Error in logDailyActiveIfNeeded:', err);
|
|
238
140
|
}
|
|
239
141
|
}
|
|
142
|
+
async logServerError(args) {
|
|
143
|
+
this.#watchdog.send({
|
|
144
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
145
|
+
payload: {
|
|
146
|
+
mcp_client: this.#mcpClient,
|
|
147
|
+
server_error: {
|
|
148
|
+
tool_name: args.toolName
|
|
149
|
+
? stripUnderscoreBeforeNumber(args.toolName)
|
|
150
|
+
: '',
|
|
151
|
+
error_code: args.errorCode,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
240
156
|
#shouldLogDailyActive(state) {
|
|
241
157
|
if (!state.lastActive) {
|
|
242
158
|
return true;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT:
|
|
7
|
+
* 1. this module must only contain ErrorCode.
|
|
8
|
+
* 2. do not refactor ErrorCode to elsewhere.
|
|
9
|
+
* 3. prefix new enum values with "ERROR_CODE_". This makes it easier to
|
|
10
|
+
* programmtically parse this file.
|
|
11
|
+
*/
|
|
12
|
+
export var ErrorCode;
|
|
13
|
+
(function (ErrorCode) {
|
|
14
|
+
ErrorCode[ErrorCode["ERROR_CODE_UNSPECIFIED"] = 0] = "ERROR_CODE_UNSPECIFIED";
|
|
15
|
+
ErrorCode[ErrorCode["ERROR_CODE_PERSISTENCE_FILE_READ_FAILED"] = 1] = "ERROR_CODE_PERSISTENCE_FILE_READ_FAILED";
|
|
16
|
+
ErrorCode[ErrorCode["ERROR_CODE_PERSISTENCE_FILE_SAVE_FAILED"] = 2] = "ERROR_CODE_PERSISTENCE_FILE_SAVE_FAILED";
|
|
17
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|