diff --git a/src/index.ts b/src/index.ts index 8fed38b83..105a311e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -254,6 +254,8 @@ export async function createMcpServer( } finally { void clearcutLogger?.logToolInvocation({ toolName: tool.name, + params, + schema, success, latencyMs: bucketizeLatency(Date.now() - startTime), }); diff --git a/src/telemetry/ClearcutLogger.ts b/src/telemetry/ClearcutLogger.ts index c2e2b5b75..27a499d91 100644 --- a/src/telemetry/ClearcutLogger.ts +++ b/src/telemetry/ClearcutLogger.ts @@ -17,6 +17,7 @@ import { type FlagUsage, WatchdogMessageType, OsType, + type ToolInvocation, } from './types.js'; import {WatchdogClient} from './WatchdogClient.js'; @@ -207,18 +208,27 @@ export class ClearcutLogger { async logToolInvocation(args: { toolName: string; + params: ShapeOutput; + schema: zod.ZodRawShape; success: boolean; latencyMs: number; }): Promise { + const tool_invocation: ToolInvocation = { + tool_name: args.toolName, + success: args.success, + latency_ms: args.latencyMs, + }; + if (Object.keys(args.params).length > 0) { + tool_invocation.tool_params = { + [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema), + }; + } + this.#watchdog.send({ type: WatchdogMessageType.LOG_EVENT, payload: { mcp_client: this.#mcpClient, - tool_invocation: { - tool_name: args.toolName, - success: args.success, - latency_ms: args.latencyMs, - }, + tool_invocation: tool_invocation, }, }); } diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index 8acb32922..d3caf3934 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -22,6 +22,7 @@ export interface ToolInvocation { tool_name: string; success: boolean; latency_ms: number; + tool_params?: object; } export interface ServerStart { diff --git a/tests/telemetry/ClearcutLogger.test.ts b/tests/telemetry/ClearcutLogger.test.ts index 3f49d6dec..8ef061bdd 100644 --- a/tests/telemetry/ClearcutLogger.test.ts +++ b/tests/telemetry/ClearcutLogger.test.ts @@ -46,6 +46,8 @@ describe('ClearcutLogger', () => { }); await logger.logToolInvocation({ toolName: 'test_tool', + params: {}, + schema: {}, success: true, latencyMs: 123, }); @@ -57,6 +59,40 @@ describe('ClearcutLogger', () => { assert.strictEqual(msg.payload.tool_invocation?.success, true); assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123); }); + it('sends sanitized params', async () => { + const logger = new ClearcutLogger({ + persistence: mockPersistence, + appVersion: '1.0.0', + watchdogClient: mockWatchdogClient, + }); + + const schema = { + uid: zod.string(), + myString: zod.string(), + }; + + const params = { + uid: 'sensitive', + myString: 'hello', + }; + + await logger.logToolInvocation({ + toolName: 'test_tool', + params, + schema, + success: true, + latencyMs: 123, + }); + + assert(mockWatchdogClient.send.calledOnce); + const msg = mockWatchdogClient.send.firstCall.args[0]; + assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT); + assert.deepStrictEqual(msg.payload.tool_invocation?.tool_params, { + test_tool_params: { + my_string_length: 5, + }, + }); + }); }); describe('setClientName', () => {