Skip to content

Commit 435eae9

Browse files
committed
chore: add function to sanitize params for tool calls
1 parent 0a7c0a7 commit 435eae9

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
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",
3030
"verify-npm-package": "node scripts/verify-npm-package.mjs",
31+
"watch": "tsc --watch",
3132
"eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts",
3233
"count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
3334
},

src/telemetry/ClearcutLogger.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import process from 'node:process';
88

99
import {DAEMON_CLIENT_NAME} from '../daemon/utils.js';
1010
import {logger} from '../logger.js';
11+
import {zod, type ShapeOutput} from '../third_party/index.js';
1112

1213
import type {LocalState, Persistence} from './persistence.js';
1314
import {FilePersistence} from './persistence.js';
@@ -20,6 +21,99 @@ import {
2021
import {WatchdogClient} from './WatchdogClient.js';
2122

2223
const MS_PER_DAY = 24 * 60 * 60 * 1000;
24+
const PARAM_BLOCKLIST = new Set(['uid']);
25+
26+
const SUPPORTED_ZOD_TYPES = [
27+
'ZodString',
28+
'ZodNumber',
29+
'ZodBoolean',
30+
'ZodArray',
31+
'ZodEnum',
32+
] as const;
33+
type ZodType = typeof SUPPORTED_ZOD_TYPES[number];
34+
35+
function isZodType(type: string): type is ZodType {
36+
return SUPPORTED_ZOD_TYPES.includes(type as ZodType);
37+
}
38+
39+
function getZodType(zodType: any): ZodType {
40+
const def = zodType._def;
41+
const typeName = def.typeName;
42+
43+
if (
44+
typeName === 'ZodOptional' ||
45+
typeName === 'ZodDefault' ||
46+
typeName === 'ZodNullable'
47+
) {
48+
return getZodType(def.innerType);
49+
}
50+
if (typeName === 'ZodEffects') {
51+
return getZodType(def.schema);
52+
}
53+
54+
if (isZodType(typeName)) {
55+
return typeName;
56+
}
57+
throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
58+
}
59+
60+
type LoggedToolCallArgValue = string | number | boolean;
61+
62+
function transformName(zodType: ZodType, name: string): string {
63+
if (zodType === 'ZodString') {
64+
return `${name}_length`;
65+
} else if (zodType === 'ZodArray') {
66+
return `${name}_count`;
67+
} else {
68+
return name;
69+
}
70+
}
71+
72+
function transformValue(zodType: ZodType, value: unknown): LoggedToolCallArgValue {
73+
if (zodType === 'ZodString') {
74+
return (value as string).length;
75+
} else if (zodType === 'ZodArray') {
76+
return (value as Array<unknown>).length;
77+
} else {
78+
return value as LoggedToolCallArgValue;
79+
}
80+
}
81+
82+
function hasEquivalentType(zodType: ZodType, value: unknown): boolean {
83+
if (zodType === 'ZodString') {
84+
return typeof value === 'string';
85+
} else if (zodType === 'ZodArray') {
86+
return Array.isArray(value);
87+
} else if (zodType === 'ZodNumber') {
88+
return typeof value === 'number';
89+
} else if (zodType === 'ZodBoolean') {
90+
return typeof value === 'boolean';
91+
} else if (zodType === 'ZodEnum') {
92+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
93+
} else {
94+
return false;
95+
}
96+
}
97+
98+
export function sanitizeParams(
99+
params: ShapeOutput<zod.ZodRawShape>,
100+
schema: zod.ZodRawShape,
101+
): ShapeOutput<zod.ZodRawShape> {
102+
const transformed: ShapeOutput<zod.ZodRawShape> = {};
103+
for (const [name, value] of Object.entries(params)) {
104+
if (PARAM_BLOCKLIST.has(name)) {
105+
continue;
106+
}
107+
const zodType = getZodType(schema[name]);
108+
if (!hasEquivalentType(zodType, value)) {
109+
throw new Error(`parameter ${name} has type ${zodType} but value ${value} is not of equivalent type`);
110+
}
111+
const transformedName = transformName(zodType, name);
112+
const transformedValue = transformValue(zodType, value);
113+
transformed[transformedName] = transformedValue;
114+
}
115+
return transformed;
116+
}
23117

24118
function detectOsType(): OsType {
25119
switch (process.platform) {

src/third_party/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {hideBin} from 'yargs/helpers';
1919
export {default as debug} from 'debug';
2020
export type {Debugger} from 'debug';
2121
export {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
22+
export {type ShapeOutput} from '@modelcontextprotocol/sdk/server/zod-compat.js';
2223
export {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
2324
export {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
2425
export {Client} from '@modelcontextprotocol/sdk/client/index.js';

tests/telemetry/ClearcutLogger.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import {describe, it, afterEach, beforeEach} from 'node:test';
1010
import sinon from 'sinon';
1111

1212
import {DAEMON_CLIENT_NAME} from '../../src/daemon/utils.js';
13-
import {ClearcutLogger} from '../../src/telemetry/ClearcutLogger.js';
13+
import {
14+
ClearcutLogger,
15+
sanitizeParams,
16+
} from '../../src/telemetry/ClearcutLogger.js';
1417
import type {Persistence} from '../../src/telemetry/persistence.js';
1518
import {FilePersistence} from '../../src/telemetry/persistence.js';
1619
import {WatchdogMessageType} from '../../src/telemetry/types.js';
1720
import {WatchdogClient} from '../../src/telemetry/WatchdogClient.js';
21+
import {zod} from '../../src/third_party/index.js';
1822

1923
describe('ClearcutLogger', () => {
2024
let mockPersistence: sinon.SinonStubbedInstance<Persistence>;
@@ -163,4 +167,50 @@ describe('ClearcutLogger', () => {
163167
assert(mockPersistence.saveState.called);
164168
});
165169
});
170+
171+
describe('sanitizeParams', () => {
172+
it('filters out uid and transforms strings and arrays', () => {
173+
const schema = {
174+
uid: zod.string(),
175+
myString: zod.string(),
176+
myArray: zod.array(zod.string()),
177+
myNumber: zod.number(),
178+
myBool: zod.boolean(),
179+
myEnum: zod.enum(['a', 'b']),
180+
};
181+
182+
const params = {
183+
uid: 'sensitive',
184+
myString: 'hello',
185+
myArray: ['one', 'two'],
186+
myNumber: 42,
187+
myBool: true,
188+
myEnum: 'a' as const,
189+
};
190+
191+
const sanitized = sanitizeParams(params, schema);
192+
193+
assert.deepStrictEqual(sanitized, {
194+
myString_length: 5,
195+
myArray_count: 2,
196+
myNumber: 42,
197+
myBool: true,
198+
myEnum: 'a',
199+
});
200+
});
201+
202+
it('throws error for unsupported types', () => {
203+
const schema = {
204+
myObj: zod.object({foo: zod.string()}),
205+
};
206+
const params = {
207+
myObj: {foo: 'bar'},
208+
};
209+
210+
assert.throws(
211+
() => sanitizeParams(params, schema),
212+
/Unsupported zod type for tool parameter: ZodObject/,
213+
);
214+
});
215+
});
166216
});

0 commit comments

Comments
 (0)