Skip to content

Commit 20bbd55

Browse files
committed
Add a script to generate tool_call_metrics.json for telemetry.
1 parent f382b7d commit 20bbd55

6 files changed

Lines changed: 769 additions & 5 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"typecheck": "tsc --noEmit",
1717
"format": "eslint --cache --fix . && prettier --write --cache .",
1818
"check-format": "eslint --cache . && prettier --check --cache .;",
19-
"gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run format",
19+
"gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-tool-call-metrics && npm run format",
2020
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
2121
"start": "npm run build && node build/src/index.js",
2222
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
@@ -27,6 +27,7 @@
2727
"prepare": "node --experimental-strip-types scripts/prepare.ts",
2828
"verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts",
2929
"update-lighthouse": "node --experimental-strip-types scripts/update-lighthouse.ts",
30+
"update-tool-call-metrics": "node --experimental-strip-types scripts/update_tool_call_metrics.ts",
3031
"verify-npm-package": "node scripts/verify-npm-package.mjs",
3132
"eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts",
3233
"count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as fs from 'node:fs';
8+
import * as path from 'node:path';
9+
10+
import type {ParsedArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
11+
import {generateToolMetrics} from '../build/src/telemetry/toolMetricsUtils.js';
12+
import type {ToolDefinition} from '../build/src/tools/ToolDefinition.js';
13+
import {createTools} from '../build/src/tools/tools.js';
14+
15+
export function HaveUniqueNames(tools: ToolDefinition[]): boolean {
16+
const toolNames = tools.map(tool => tool.name);
17+
const toolNamesSet = new Set(toolNames);
18+
return toolNamesSet.size === toolNames.length;
19+
}
20+
21+
function writeToolCallMetricsConfig() {
22+
const outputPath = path.resolve('src/telemetry/tool_call_metrics.json');
23+
24+
const dir = path.dirname(outputPath);
25+
if (!fs.existsSync(dir)) {
26+
throw new Error(`Error: Directory ${dir} does not exist.`);
27+
}
28+
29+
const fullTools = createTools({slim: false} as ParsedArguments);
30+
const slimTools = createTools({slim: true} as ParsedArguments);
31+
32+
const allTools = [...fullTools, ...slimTools];
33+
34+
if (!HaveUniqueNames(allTools)) {
35+
throw new Error('Error: Duplicate tool names found.');
36+
}
37+
38+
// Map tools to their metadata
39+
const toolData = generateToolMetrics(allTools);
40+
41+
// Sort by name for determinism
42+
toolData.sort((a, b) => a.name.localeCompare(b.name));
43+
44+
fs.writeFileSync(outputPath, JSON.stringify(toolData, null, 2) + '\n');
45+
46+
console.log(
47+
`Successfully wrote ${toolData.length} tool names with arguments to ${outputPath}`,
48+
);
49+
}
50+
51+
writeToolCallMetricsConfig();

src/telemetry/ClearcutLogger.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import {WatchdogClient} from './WatchdogClient.js';
2222

2323
const MS_PER_DAY = 24 * 60 * 60 * 1000;
24-
const PARAM_BLOCKLIST = new Set(['uid']);
24+
export const PARAM_BLOCKLIST = new Set(['uid', 'reqid', 'msgid']);
2525

2626
const SUPPORTED_ZOD_TYPES = [
2727
'ZodString',
@@ -36,7 +36,7 @@ function isZodType(type: string): type is ZodType {
3636
return SUPPORTED_ZOD_TYPES.includes(type as ZodType);
3737
}
3838

39-
function getZodType(zodType: zod.ZodTypeAny): ZodType {
39+
export function getZodType(zodType: zod.ZodTypeAny): ZodType {
4040
const def = zodType._def;
4141
const typeName = def.typeName;
4242

@@ -59,7 +59,7 @@ function getZodType(zodType: zod.ZodTypeAny): ZodType {
5959

6060
type LoggedToolCallArgValue = string | number | boolean;
6161

62-
function transformName(zodType: ZodType, name: string): string {
62+
export function transformArgName(zodType: ZodType, name: string): string {
6363
if (zodType === 'ZodString') {
6464
return `${name}_length`;
6565
} else if (zodType === 'ZodArray') {
@@ -69,6 +69,22 @@ function transformName(zodType: ZodType, name: string): string {
6969
}
7070
}
7171

72+
export function transformArgType(zodType: ZodType): string {
73+
if (zodType === 'ZodString' || zodType === 'ZodArray') {
74+
return 'number';
75+
}
76+
switch (zodType) {
77+
case 'ZodNumber':
78+
return 'number';
79+
case 'ZodBoolean':
80+
return 'boolean';
81+
case 'ZodEnum':
82+
return 'enum';
83+
default:
84+
throw new Error(`Unsupported zod type for tool parameter: ${zodType}`);
85+
}
86+
}
87+
7288
function transformValue(
7389
zodType: ZodType,
7490
value: unknown,
@@ -117,7 +133,7 @@ export function sanitizeParams(
117133
`parameter ${name} has type ${zodType} but value ${value} is not of equivalent type`,
118134
);
119135
}
120-
const transformedName = transformName(zodType, name);
136+
const transformedName = transformArgName(zodType, name);
121137
const transformedValue = transformValue(zodType, value);
122138
transformed[transformedName] = transformedValue;
123139
}

src/telemetry/toolMetricsUtils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type {ToolDefinition} from '../tools/ToolDefinition.js';
8+
9+
import {
10+
transformArgName,
11+
transformArgType,
12+
getZodType,
13+
PARAM_BLOCKLIST,
14+
} from './ClearcutLogger.js';
15+
16+
/**
17+
* Validates that all values in an enum are of the homogeneous primitive type.
18+
* Returns the primitive type string. Throws an error if heterogeneous.
19+
*/
20+
export function validateEnumHomogeneity(values: unknown[]): string {
21+
const firstType = typeof values[0];
22+
for (const val of values) {
23+
if (typeof val !== firstType) {
24+
throw new Error('Heterogeneous enum types found');
25+
}
26+
}
27+
return firstType;
28+
}
29+
30+
export interface ArgMetric {
31+
name: string;
32+
argType: string;
33+
}
34+
35+
export interface ToolMetric {
36+
name: string;
37+
args: ArgMetric[];
38+
}
39+
40+
/**
41+
* Generates tool metrics from tool definitions.
42+
*/
43+
export function generateToolMetrics(tools: ToolDefinition[]): ToolMetric[] {
44+
return tools.map(tool => {
45+
const args: ArgMetric[] = [];
46+
47+
for (const [name, schema] of Object.entries(tool.schema)) {
48+
if (PARAM_BLOCKLIST.has(name)) {
49+
continue;
50+
}
51+
const zodType = getZodType(schema);
52+
const transformedName = transformArgName(zodType, name);
53+
let argType = transformArgType(zodType);
54+
55+
if (zodType === 'ZodEnum' && schema._def.values?.length > 0) {
56+
argType = validateEnumHomogeneity(schema._def.values);
57+
}
58+
59+
args.push({
60+
name: transformedName,
61+
argType,
62+
});
63+
}
64+
65+
return {
66+
name: tool.name,
67+
args,
68+
};
69+
});
70+
}

0 commit comments

Comments
 (0)