Skip to content

Commit 65b3c68

Browse files
committed
chore: add function to sanitize params for tool calls
1 parent b1684c6 commit 65b3c68

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

src/telemetry/ClearcutLogger.ts

Lines changed: 73 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,78 @@ import {
2021
import {WatchdogClient} from './WatchdogClient.js';
2122

2223
const MS_PER_DAY = 24 * 60 * 60 * 1000;
24+
const PARAM_BLOCKLIST = ['uid'];
25+
26+
function getZodType(zodType: any): string {
27+
const def = zodType._def;
28+
const typeName = def.typeName;
29+
30+
if (
31+
typeName === 'ZodOptional' ||
32+
typeName === 'ZodDefault' ||
33+
typeName === 'ZodNullable'
34+
) {
35+
return getZodType(def.innerType);
36+
}
37+
if (typeName === 'ZodEffects') {
38+
return getZodType(def.schema);
39+
}
40+
41+
let type: string;
42+
switch (typeName) {
43+
case 'ZodString':
44+
return 'string';
45+
case 'ZodNumber':
46+
return 'number';
47+
case 'ZodBoolean':
48+
return 'boolean';
49+
case 'ZodArray':
50+
return 'array';
51+
case 'ZodEnum':
52+
return 'enum';
53+
default:
54+
throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
55+
}
56+
}
57+
58+
function filterBlockedParams(
59+
params: ShapeOutput<zod.ZodRawShape>,
60+
): ShapeOutput<zod.ZodRawShape> {
61+
const filteredParams = {...params};
62+
for (const key of PARAM_BLOCKLIST) {
63+
if (key in filteredParams) {
64+
delete filteredParams[key];
65+
}
66+
}
67+
return filteredParams;
68+
}
69+
70+
function transformParams(
71+
params: Record<string, any>,
72+
schema: zod.ZodRawShape,
73+
): Record<string, any> {
74+
const transformed: Record<string, any> = {};
75+
for (const [name, value] of Object.entries(params)) {
76+
const zodType = getZodType(schema[name]);
77+
78+
if (zodType === 'string') {
79+
transformed[`${name}_length`] = (value as string).length;
80+
} else if (zodType === 'array') {
81+
transformed[`${name}_count`] = (value as any[]).length;
82+
} else {
83+
transformed[name] = value;
84+
}
85+
}
86+
return transformed;
87+
}
88+
89+
export function sanitizeParams(
90+
params: ShapeOutput<zod.ZodRawShape>,
91+
schema: zod.ZodRawShape,
92+
): Record<string, any> {
93+
const filtered = filterBlockedParams(params);
94+
return transformParams(filtered, schema);
95+
}
2396

2497
function detectOsType(): OsType {
2598
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)