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.
Files changed (38) hide show
  1. package/README.md +55 -6
  2. package/build/src/DevtoolsUtils.js +13 -0
  3. package/build/src/HeapSnapshotManager.js +26 -2
  4. package/build/src/McpContext.js +3 -0
  5. package/build/src/McpResponse.js +51 -21
  6. package/build/src/ToolHandler.js +217 -0
  7. package/build/src/WaitForHelper.js +18 -4
  8. package/build/src/bin/check-latest-version.js +25 -1
  9. package/build/src/bin/chrome-devtools-cli-options.js +38 -2
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
  11. package/build/src/bin/chrome-devtools-mcp-main.js +4 -3
  12. package/build/src/bin/chrome-devtools.js +0 -2
  13. package/build/src/daemon/client.js +12 -6
  14. package/build/src/formatters/HeapSnapshotFormatter.js +27 -6
  15. package/build/src/index.js +11 -164
  16. package/build/src/telemetry/ClearcutLogger.js +34 -118
  17. package/build/src/telemetry/errors.js +18 -0
  18. package/build/src/telemetry/flagUtils.js +4 -3
  19. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  20. package/build/src/telemetry/persistence.js +20 -2
  21. package/build/src/telemetry/transformation.js +134 -0
  22. package/build/src/telemetry/types.js +0 -8
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +140 -857
  24. package/build/src/third_party/bundled-packages.json +3 -3
  25. package/build/src/third_party/devtools-formatter-worker.js +475 -146
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +39 -44
  27. package/build/src/third_party/index.js +4055 -30401
  28. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
  30. package/build/src/tools/ToolDefinition.js +1 -1
  31. package/build/src/tools/emulation.js +3 -2
  32. package/build/src/tools/input.js +46 -16
  33. package/build/src/tools/lighthouse.js +7 -7
  34. package/build/src/tools/memory.js +24 -0
  35. package/build/src/tools/script.js +32 -10
  36. package/build/src/version.js +1 -1
  37. package/package.json +10 -7
  38. 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>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.',
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 senstive before returning to the client.',
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, clearcutLogger } = await createMcpServer(args, {
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 clearcutLogger?.logDailyActiveIfNeeded();
32
- void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));
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 && isNodeLike(items[0])) {
18
- lines.push('id,name,type,distance,selfSize,retainedSize');
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},"${item.name}",${item.type},${item.distance},${item.selfSize},${item.retainedSize}`);
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},"${info.name}",${info.count},${info.self},${info.maxRet}`);
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) {
@@ -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 { bucketizeLatency } from './telemetry/metricUtils.js';
12
+ import { FilePersistence } from './telemetry/persistence.js';
15
13
  import { McpServer, SetLevelRequestSchema, ListRootsResultSchema, RootsListChangedNotificationSchema, } from './third_party/index.js';
16
- import { labels, OFF_BY_DEFAULT_CATEGORIES } from './tools/categories.js';
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 function buildFlag(category) {
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
- clearcutLogger = new ClearcutLogger({
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
- clearcutLogger?.setClientName(clientName);
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 { disabled, reason: disabledReason } = getToolStatusInfo(tool, serverArgs);
168
- if (disabled && !serverArgs.viaCli) {
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: schema,
114
+ inputSchema: toolHandler.registeredInputSchema,
180
115
  annotations: tool.annotations,
181
116
  }, async (params) => {
182
- if (disabledReason) {
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, clearcutLogger };
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 { FilePersistence } from './persistence.js';
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 ?? new FilePersistence();
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: args.toolName,
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
- [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema),
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