Skip to content

Commit 8767ff2

Browse files
authored
v0.1.6
1 parent e5e0c1c commit 8767ff2

File tree

12 files changed

+1124
-35
lines changed

12 files changed

+1124
-35
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcpcat",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights",
55
"type": "module",
66
"main": "dist/index.js",

src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { setTelemetryManager } from "./modules/eventQueue.js";
3030
* @param options.enableReportMissing - Adds a "get_more_tools" tool that allows LLMs to automatically report missing functionality.
3131
* @param options.enableTracing - Enables tracking of tool calls and usage patterns.
3232
* @param options.enableToolCallContext - Injects a "context" parameter to existing tools to capture user intent.
33+
* @param options.customContextDescription - Custom description for the injected context parameter. Only applies when enableToolCallContext is true. Use this to provide domain-specific guidance to LLMs about what context they should provide.
3334
* @param options.identify - Async function to identify users and attach custom data to their sessions.
3435
* @param options.redactSensitiveInformation - Function to redact sensitive data before sending to MCPCat.
3536
* @param options.exporters - Configure telemetry exporters to send events to external systems. Available exporters:
@@ -76,6 +77,15 @@ import { setTelemetryManager } from "./modules/eventQueue.js";
7677
*
7778
* @example
7879
* ```typescript
80+
* // With custom context description
81+
* mcpcat.track(mcpServer, "proj_abc123xyz", {
82+
* enableToolCallContext: true,
83+
* customContextDescription: "Explain why you're calling this tool and what business objective it helps achieve"
84+
* });
85+
* ```
86+
*
87+
* @example
88+
* ```typescript
7989
* // With sensitive data redaction
8090
* mcpcat.track(mcpServer, "proj_abc123xyz", {
8191
* redactSensitiveInformation: async (text) => {
@@ -153,6 +163,7 @@ function track(
153163
enableReportMissing: options.enableReportMissing ?? true,
154164
enableTracing: options.enableTracing ?? true,
155165
enableToolCallContext: options.enableToolCallContext ?? true,
166+
customContextDescription: options.customContextDescription,
156167
identify: options.identify,
157168
redactSensitiveInformation: options.redactSensitiveInformation,
158169
},

src/modules/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
// MCPCat Settings
22
export const INACTIVITY_TIMEOUT_IN_MINUTES = 30;
3+
export const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION =
4+
"Describe why you are calling this tool and how it fits into your overall task";

src/modules/context-parameters.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RegisteredTool } from "../types";
22
import { z } from "zod";
3+
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from "./constants";
34

45
// Detect if something is a Zod schema (has _def and parse methods)
56
function isZodSchema(schema: any): boolean {
@@ -23,6 +24,7 @@ function isShorthandZodSyntax(schema: any): boolean {
2324

2425
export function addContextParameterToTool(
2526
tool: RegisteredTool,
27+
customContextDescription?: string,
2628
): RegisteredTool {
2729
// Create a shallow copy of the tool to avoid modifying the original
2830
const modifiedTool = { ...tool };
@@ -55,7 +57,7 @@ export function addContextParameterToTool(
5557
context: z
5658
.string()
5759
.describe(
58-
"Describe why you are calling this tool and how it fits into your overall task",
60+
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
5961
),
6062
});
6163

@@ -86,7 +88,7 @@ export function addContextParameterToTool(
8688
const contextField = z
8789
.string()
8890
.describe(
89-
"Describe why you are calling this tool and how it fits into your overall task",
91+
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
9092
);
9193

9294
// Create new z.object with context and all original fields
@@ -114,7 +116,7 @@ export function addContextParameterToTool(
114116
modifiedTool.inputSchema.properties.context = {
115117
type: "string",
116118
description:
117-
"Describe why you are calling this tool and how it fits into your overall task",
119+
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
118120
};
119121

120122
// Add context to required array if it exists
@@ -133,12 +135,13 @@ export function addContextParameterToTool(
133135

134136
export function addContextParameterToTools(
135137
tools: RegisteredTool[],
138+
customContextDescription?: string,
136139
): RegisteredTool[] {
137140
return tools.map((tool) => {
138141
// Skip get_more_tools - it has its own special context parameter
139142
if ((tool as any).name === "get_more_tools") {
140143
return tool;
141144
}
142-
return addContextParameterToTool(tool);
145+
return addContextParameterToTool(tool, customContextDescription);
143146
});
144147
}

src/modules/internal.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MCPCatData, MCPServerLike } from "../types.js";
1+
import { MCPCatData, MCPServerLike, UserIdentity } from "../types.js";
22

33
// Internal tracking storage
44
const _serverTracking = new WeakMap<MCPServerLike, MCPCatData>();
@@ -15,3 +15,49 @@ export function setServerTrackingData(
1515
): void {
1616
_serverTracking.set(server, data);
1717
}
18+
19+
/**
20+
* Deep comparison of two UserIdentity objects
21+
*/
22+
export function areIdentitiesEqual(a: UserIdentity, b: UserIdentity): boolean {
23+
if (a.userId !== b.userId) return false;
24+
if (a.userName !== b.userName) return false;
25+
26+
// Deep compare userData objects
27+
const aData = a.userData || {};
28+
const bData = b.userData || {};
29+
30+
const aKeys = Object.keys(aData);
31+
const bKeys = Object.keys(bData);
32+
33+
if (aKeys.length !== bKeys.length) return false;
34+
35+
for (const key of aKeys) {
36+
if (!(key in bData)) return false;
37+
if (JSON.stringify(aData[key]) !== JSON.stringify(bData[key])) return false;
38+
}
39+
40+
return true;
41+
}
42+
43+
/**
44+
* Merges two UserIdentity objects, overwriting userId and userName,
45+
* but merging userData fields
46+
*/
47+
export function mergeIdentities(
48+
previous: UserIdentity | undefined,
49+
next: UserIdentity,
50+
): UserIdentity {
51+
if (!previous) {
52+
return next;
53+
}
54+
55+
return {
56+
userId: next.userId,
57+
userName: next.userName,
58+
userData: {
59+
...(previous.userData || {}),
60+
...(next.userData || {}),
61+
},
62+
};
63+
}

src/modules/tools.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ export function setupMCPCatTools(server: MCPServerLike): void {
9797

9898
// Add context parameter to all existing tools if enableToolCallContext is true
9999
if (data.options.enableToolCallContext) {
100-
tools = addContextParameterToTools(tools);
100+
tools = addContextParameterToTools(
101+
tools,
102+
data.options.customContextDescription,
103+
);
101104
}
102105

103106
// Add report_missing tool if enabled

src/modules/tracing.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
} from "../types.js";
1212
import { writeToLog } from "./logging.js";
1313
import { handleReportMissing } from "./tools.js";
14-
import { getServerTrackingData } from "./internal.js";
14+
import {
15+
getServerTrackingData,
16+
areIdentitiesEqual,
17+
mergeIdentities,
18+
} from "./internal.js";
1519
import { getServerSessionId } from "./session.js";
1620
import { PublishEventRequestEventTypeEnum } from "mcpcat-api";
1721
import { publishEvent } from "./eventQueue.js";
@@ -223,22 +227,38 @@ export function setupToolCallTracing(server: MCPServerLike): void {
223227

224228
try {
225229
// Try to identify the session if we haven't already and identify function is provided
226-
if (
227-
data.options.identify &&
228-
data.identifiedSessions.get(sessionId) === undefined
229-
) {
230+
if (data.options.identify) {
230231
let identifyEvent: UnredactedEvent = {
231232
...event,
232233
eventType: PublishEventRequestEventTypeEnum.mcpcatIdentify,
233234
};
234235
try {
235236
const identityResult = await data.options.identify(request, extra);
236237
if (identityResult) {
237-
writeToLog(
238-
`Identified session ${sessionId} with identity: ${JSON.stringify(identityResult)}`,
238+
// Get previous identity for this session
239+
const previousIdentity = data.identifiedSessions.get(sessionId);
240+
241+
// Merge identities (overwrite userId/userName, merge userData)
242+
const mergedIdentity = mergeIdentities(
243+
previousIdentity,
244+
identityResult,
239245
);
240-
data.identifiedSessions.set(sessionId, identityResult);
241-
publishEvent(server, identifyEvent);
246+
247+
// Only publish if identity has changed
248+
const hasChanged =
249+
!previousIdentity ||
250+
!areIdentitiesEqual(previousIdentity, mergedIdentity);
251+
252+
// Always update the stored identity with the merged version FIRST
253+
// so that publishEvent can get the latest identity in sessionInfo
254+
data.identifiedSessions.set(sessionId, mergedIdentity);
255+
256+
if (hasChanged) {
257+
writeToLog(
258+
`Identified session ${sessionId} with identity: ${JSON.stringify(mergedIdentity)}`,
259+
);
260+
publishEvent(server, identifyEvent);
261+
}
242262
} else {
243263
writeToLog(
244264
`Warning: Supplied identify function returned null for session ${sessionId}`,

src/modules/tracingV2.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
CompatibleRequestHandlerExtra,
99
} from "../types.js";
1010
import { writeToLog } from "./logging.js";
11-
import { getServerTrackingData } from "./internal.js";
11+
import {
12+
getServerTrackingData,
13+
areIdentitiesEqual,
14+
mergeIdentities,
15+
} from "./internal.js";
1216
import { getServerSessionId } from "./session.js";
1317
import { PublishEventRequestEventTypeEnum } from "mcpcat-api";
1418
import { publishEvent } from "./eventQueue.js";
@@ -29,12 +33,15 @@ function isToolResultError(result: any): boolean {
2933

3034
function addContextParametersToToolRegistry(
3135
tools: Record<string, RegisteredTool>,
36+
customContextDescription?: string,
3237
): Record<string, RegisteredTool> {
3338
return Object.fromEntries(
3439
Object.entries(tools).map(([name, tool]) => [
3540
name,
3641
// Skip get_more_tools - it has its own context parameter
37-
name === "get_more_tools" ? tool : addContextParameterToTool(tool),
42+
name === "get_more_tools"
43+
? tool
44+
: addContextParameterToTool(tool, customContextDescription),
3845
]),
3946
);
4047
}
@@ -97,7 +104,10 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
97104
data.options.enableToolCallContext &&
98105
property !== "get_more_tools"
99106
) {
100-
value = addContextParameterToTool(value);
107+
value = addContextParameterToTool(
108+
value,
109+
data.options.customContextDescription,
110+
);
101111
}
102112

103113
// Apply tracing to the callback
@@ -321,23 +331,39 @@ function addTracingToToolCallback(
321331
};
322332

323333
try {
324-
// Try to identify the session if we haven't already and identify function is provided
325-
if (
326-
data.options.identify &&
327-
data.identifiedSessions.get(sessionId) === undefined
328-
) {
334+
// Try to identify the session if identify function is provided
335+
if (data.options.identify) {
329336
let identifyEvent: UnredactedEvent = {
330337
...event,
331338
eventType: PublishEventRequestEventTypeEnum.mcpcatIdentify,
332339
};
333340
try {
334341
const identityResult = await data.options.identify(request, extra);
335342
if (identityResult) {
336-
writeToLog(
337-
`Identified session ${sessionId} with identity: ${JSON.stringify(identityResult)}`,
343+
// Get previous identity for this session
344+
const previousIdentity = data.identifiedSessions.get(sessionId);
345+
346+
// Merge identities (overwrite userId/userName, merge userData)
347+
const mergedIdentity = mergeIdentities(
348+
previousIdentity,
349+
identityResult,
338350
);
339-
data.identifiedSessions.set(sessionId, identityResult);
340-
publishEvent(lowLevelServer, identifyEvent);
351+
352+
// Only publish if identity has changed
353+
const hasChanged =
354+
!previousIdentity ||
355+
!areIdentitiesEqual(previousIdentity, mergedIdentity);
356+
357+
// Always update the stored identity with the merged version FIRST
358+
// so that publishEvent can get the latest identity in sessionInfo
359+
data.identifiedSessions.set(sessionId, mergedIdentity);
360+
361+
if (hasChanged) {
362+
writeToLog(
363+
`Identified session ${sessionId} with identity: ${JSON.stringify(mergedIdentity)}`,
364+
);
365+
publishEvent(lowLevelServer, identifyEvent);
366+
}
341367
} else {
342368
writeToLog(
343369
`Warning: Supplied identify function returned null for session ${sessionId}`,
@@ -462,6 +488,7 @@ export function setupTracking(server: HighLevelMCPServerLike): void {
462488
if (mcpcatData?.options.enableToolCallContext) {
463489
server._registeredTools = addContextParametersToToolRegistry(
464490
server._registeredTools,
491+
mcpcatData.options.customContextDescription,
465492
);
466493
}
467494

src/tests/context-parameters.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@modelcontextprotocol/sdk/types";
1212
import { EventCapture } from "./test-utils";
1313
import { PublishEventRequestEventTypeEnum } from "mcpcat-api";
14+
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from "../modules/constants";
1415

1516
describe("Context Parameters", () => {
1617
let server: any;
@@ -348,7 +349,39 @@ describe("Context Parameters", () => {
348349
expect(tool.inputSchema.properties.context).toBeDefined();
349350
expect(tool.inputSchema.properties.context.type).toBe("string");
350351
expect(tool.inputSchema.properties.context.description).toBe(
351-
"Describe why you are calling this tool and how it fits into your overall task",
352+
DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
353+
);
354+
});
355+
});
356+
357+
it("should use default context description when no custom description is provided", async () => {
358+
// Enable tracking WITHOUT customContextDescription
359+
track(server, "test-project", {
360+
enableToolCallContext: true,
361+
});
362+
363+
// Get the tools list
364+
const toolsResponse = await client.request(
365+
{
366+
method: "tools/list",
367+
params: {},
368+
},
369+
ListToolsResultSchema,
370+
);
371+
372+
// Find all original tools
373+
const originalTools = ["add_todo", "list_todos", "complete_todo"];
374+
const toolsToCheck = toolsResponse.tools.filter((tool: any) =>
375+
originalTools.includes(tool.name),
376+
);
377+
378+
expect(toolsToCheck.length).toBe(3);
379+
380+
// Verify all tools use the default description
381+
toolsToCheck.forEach((tool: any) => {
382+
expect(tool.inputSchema.properties.context).toBeDefined();
383+
expect(tool.inputSchema.properties.context.description).toBe(
384+
DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
352385
);
353386
});
354387
});

0 commit comments

Comments
 (0)