@armature-tech/mcp-analytics 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +283 -0
- package/SKILL.md +328 -0
- package/dist/cjs/emit.d.ts +32 -0
- package/dist/cjs/emit.js +157 -0
- package/dist/cjs/events.d.ts +61 -0
- package/dist/cjs/events.js +166 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/mastra.d.ts +24 -0
- package/dist/cjs/mastra.js +59 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/recorder.d.ts +2 -0
- package/dist/cjs/recorder.js +282 -0
- package/dist/cjs/schema.d.ts +64 -0
- package/dist/cjs/schema.js +101 -0
- package/dist/cjs/server.d.ts +3 -0
- package/dist/cjs/server.js +80 -0
- package/dist/cjs/types.d.ts +179 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/utils.d.ts +16 -0
- package/dist/cjs/utils.js +72 -0
- package/dist/esm/emit.d.ts +32 -0
- package/dist/esm/emit.js +146 -0
- package/dist/esm/events.d.ts +61 -0
- package/dist/esm/events.js +154 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/mastra.d.ts +24 -0
- package/dist/esm/mastra.js +53 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/recorder.d.ts +2 -0
- package/dist/esm/recorder.js +278 -0
- package/dist/esm/schema.d.ts +64 -0
- package/dist/esm/schema.js +94 -0
- package/dist/esm/server.d.ts +3 -0
- package/dist/esm/server.js +75 -0
- package/dist/esm/types.d.ts +179 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.d.ts +16 -0
- package/dist/esm/utils.js +61 -0
- package/package.json +87 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AnalyticsEventKind, AnalyticsIngestBatch, AnalyticsIngestEvent, McpClientInfo, RequestExtra, TelemetryArgs } from "./types.js";
|
|
2
|
+
export declare const buildActorId: ({ mcpServerId, actorSeed, }: {
|
|
3
|
+
mcpServerId: string;
|
|
4
|
+
actorSeed: string;
|
|
5
|
+
}) => string;
|
|
6
|
+
export declare const buildEventId: ({ mcpServerId, actorId, requestId, kind, }: {
|
|
7
|
+
mcpServerId: string;
|
|
8
|
+
actorId: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
kind: AnalyticsEventKind;
|
|
11
|
+
}) => string;
|
|
12
|
+
export declare const buildToolCallEvent: ({ toolName, telemetry, input, output, status, durationMs, errorMessage, mcpServerId, actorId, sessionId, requestId, startedAt, finishedAt, }: {
|
|
13
|
+
toolName: string;
|
|
14
|
+
telemetry?: TelemetryArgs;
|
|
15
|
+
input: unknown;
|
|
16
|
+
output?: unknown;
|
|
17
|
+
status: "ok" | "error";
|
|
18
|
+
durationMs: number;
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
mcpServerId: string;
|
|
21
|
+
actorId: string;
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
requestId: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
finishedAt: string;
|
|
26
|
+
}) => AnalyticsIngestEvent;
|
|
27
|
+
export declare const buildSessionInitEvent: ({ mcpServerId, actorId, sessionId, requestId, startedAt, extra, clientInfo, }: {
|
|
28
|
+
mcpServerId: string;
|
|
29
|
+
actorId: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
requestId: string;
|
|
32
|
+
startedAt: string;
|
|
33
|
+
extra?: RequestExtra;
|
|
34
|
+
clientInfo?: McpClientInfo;
|
|
35
|
+
}) => AnalyticsIngestEvent;
|
|
36
|
+
export declare const buildBatch: ({ event, extra, mcpServerId, actorId, startedAt, sessionInitKeys, clientInfo, }: {
|
|
37
|
+
event: AnalyticsIngestEvent;
|
|
38
|
+
extra?: RequestExtra;
|
|
39
|
+
mcpServerId: string;
|
|
40
|
+
actorId: string;
|
|
41
|
+
startedAt: string;
|
|
42
|
+
sessionInitKeys: Set<string>;
|
|
43
|
+
clientInfo?: McpClientInfo;
|
|
44
|
+
}) => AnalyticsIngestBatch;
|
|
45
|
+
export declare const buildSessionInitBatch: ({ mcpServerId, actorId, sessionId, requestId, startedAt, extra, sessionInitKeys, clientInfo, }: {
|
|
46
|
+
mcpServerId: string;
|
|
47
|
+
actorId: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
requestId: string;
|
|
50
|
+
startedAt: string;
|
|
51
|
+
extra?: RequestExtra;
|
|
52
|
+
sessionInitKeys: Set<string>;
|
|
53
|
+
clientInfo?: McpClientInfo;
|
|
54
|
+
}) => AnalyticsIngestBatch | null;
|
|
55
|
+
export declare const normalizeSessionId: (eventSessionId: string | undefined, extra: RequestExtra | undefined) => string | undefined;
|
|
56
|
+
export declare const normalizeRequestId: (eventRequestId: string | undefined, extra: RequestExtra | undefined) => string;
|
|
57
|
+
export declare const normalizeStartedAt: ({ startedAt, durationMs, finishedAtMs, }: {
|
|
58
|
+
startedAt?: string | Date | number;
|
|
59
|
+
durationMs?: number;
|
|
60
|
+
finishedAtMs: number;
|
|
61
|
+
}) => string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { MAX_CAPABILITIES_BYTES, MAX_PREVIEW_BYTES, MAX_SOURCE_BYTES, SCHEMA_VERSION, headerValue, isRecord, sha256Hex, stringifyPreview, truncateUtf8, } from "./utils.js";
|
|
3
|
+
const trimOrUndefined = (value) => {
|
|
4
|
+
if (typeof value !== "string")
|
|
5
|
+
return undefined;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
8
|
+
};
|
|
9
|
+
const capCapabilities = (capabilities) => {
|
|
10
|
+
if (!isRecord(capabilities))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
if (JSON.stringify(capabilities).length > MAX_CAPABILITIES_BYTES)
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return capabilities;
|
|
20
|
+
};
|
|
21
|
+
export const buildActorId = ({ mcpServerId, actorSeed, }) => {
|
|
22
|
+
return sha256Hex(`${mcpServerId} ${actorSeed}`);
|
|
23
|
+
};
|
|
24
|
+
export const buildEventId = ({ mcpServerId, actorId, requestId, kind, }) => {
|
|
25
|
+
return sha256Hex(`${mcpServerId} ${actorId} ${kind} ${requestId}`);
|
|
26
|
+
};
|
|
27
|
+
const buildToolCallSource = (toolName, input) => {
|
|
28
|
+
return `MCP tool call: ${toolName}\n\nInput:\n${stringifyPreview(input)}`;
|
|
29
|
+
};
|
|
30
|
+
export const buildToolCallEvent = ({ toolName, telemetry, input, output, status, durationMs, errorMessage, mcpServerId, actorId, sessionId, requestId, startedAt, finishedAt, }) => {
|
|
31
|
+
const inputPreview = truncateUtf8(stringifyPreview(input), MAX_PREVIEW_BYTES);
|
|
32
|
+
const source = truncateUtf8(buildToolCallSource(toolName, input), MAX_SOURCE_BYTES);
|
|
33
|
+
const resultPreview = output === undefined
|
|
34
|
+
? null
|
|
35
|
+
: truncateUtf8(stringifyPreview(output), MAX_PREVIEW_BYTES);
|
|
36
|
+
return {
|
|
37
|
+
event_id: buildEventId({ mcpServerId, actorId, requestId, kind: "tool_call" }),
|
|
38
|
+
kind: "tool_call",
|
|
39
|
+
mcp_server_id: mcpServerId,
|
|
40
|
+
actor_id: actorId,
|
|
41
|
+
session_id_hint: sessionId ?? null,
|
|
42
|
+
started_at: startedAt,
|
|
43
|
+
finished_at: finishedAt,
|
|
44
|
+
duration_ms: durationMs,
|
|
45
|
+
ok: status === "ok",
|
|
46
|
+
error: errorMessage ?? null,
|
|
47
|
+
metadata: {
|
|
48
|
+
tool_name: toolName,
|
|
49
|
+
intent: telemetry?.intent ?? null,
|
|
50
|
+
context: telemetry?.context ?? null,
|
|
51
|
+
frustration_level: telemetry?.frustration_level ?? null,
|
|
52
|
+
input_preview: inputPreview.value,
|
|
53
|
+
},
|
|
54
|
+
script_source: source.value,
|
|
55
|
+
script_source_truncated: source.truncated,
|
|
56
|
+
result_preview: resultPreview?.value ?? null,
|
|
57
|
+
result_truncated: resultPreview?.truncated ?? false,
|
|
58
|
+
calls: [],
|
|
59
|
+
logs: [],
|
|
60
|
+
search_calls: [],
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export const buildSessionInitEvent = ({ mcpServerId, actorId, sessionId, requestId, startedAt, extra, clientInfo, }) => {
|
|
64
|
+
return {
|
|
65
|
+
event_id: buildEventId({ mcpServerId, actorId, requestId, kind: "session_init" }),
|
|
66
|
+
kind: "session_init",
|
|
67
|
+
mcp_server_id: mcpServerId,
|
|
68
|
+
actor_id: actorId,
|
|
69
|
+
session_id_hint: sessionId,
|
|
70
|
+
started_at: startedAt,
|
|
71
|
+
finished_at: startedAt,
|
|
72
|
+
duration_ms: 0,
|
|
73
|
+
ok: true,
|
|
74
|
+
error: null,
|
|
75
|
+
metadata: {
|
|
76
|
+
client_name: trimOrUndefined(clientInfo?.name)
|
|
77
|
+
?? trimOrUndefined(extra?.authInfo?.clientId)
|
|
78
|
+
?? null,
|
|
79
|
+
client_version: trimOrUndefined(clientInfo?.version) ?? null,
|
|
80
|
+
protocol_version: trimOrUndefined(clientInfo?.protocolVersion) ?? null,
|
|
81
|
+
capabilities: capCapabilities(clientInfo?.capabilities),
|
|
82
|
+
user_agent: headerValue(extra?.requestInfo?.headers, "user-agent"),
|
|
83
|
+
},
|
|
84
|
+
script_source: null,
|
|
85
|
+
script_source_truncated: false,
|
|
86
|
+
result_preview: null,
|
|
87
|
+
result_truncated: false,
|
|
88
|
+
calls: [],
|
|
89
|
+
logs: [],
|
|
90
|
+
search_calls: [],
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
export const buildBatch = ({ event, extra, mcpServerId, actorId, startedAt, sessionInitKeys, clientInfo, }) => {
|
|
94
|
+
const events = [];
|
|
95
|
+
if (extra?.sessionId) {
|
|
96
|
+
const key = `${mcpServerId}:${actorId}:${extra.sessionId}`;
|
|
97
|
+
if (!sessionInitKeys.has(key)) {
|
|
98
|
+
sessionInitKeys.add(key);
|
|
99
|
+
events.push(buildSessionInitEvent({
|
|
100
|
+
mcpServerId,
|
|
101
|
+
actorId,
|
|
102
|
+
sessionId: extra.sessionId,
|
|
103
|
+
requestId: `${event.event_id}:session_init`,
|
|
104
|
+
startedAt,
|
|
105
|
+
extra,
|
|
106
|
+
clientInfo,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
events.push(event);
|
|
111
|
+
return { schema_version: SCHEMA_VERSION, events };
|
|
112
|
+
};
|
|
113
|
+
export const buildSessionInitBatch = ({ mcpServerId, actorId, sessionId, requestId, startedAt, extra, sessionInitKeys, clientInfo, }) => {
|
|
114
|
+
const key = `${mcpServerId}:${actorId}:${sessionId}`;
|
|
115
|
+
if (sessionInitKeys.has(key))
|
|
116
|
+
return null;
|
|
117
|
+
sessionInitKeys.add(key);
|
|
118
|
+
return {
|
|
119
|
+
schema_version: SCHEMA_VERSION,
|
|
120
|
+
events: [
|
|
121
|
+
buildSessionInitEvent({
|
|
122
|
+
mcpServerId,
|
|
123
|
+
actorId,
|
|
124
|
+
sessionId,
|
|
125
|
+
requestId,
|
|
126
|
+
startedAt,
|
|
127
|
+
extra,
|
|
128
|
+
clientInfo,
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
export const normalizeSessionId = (eventSessionId, extra) => {
|
|
134
|
+
const explicit = trimOrUndefined(eventSessionId) ?? trimOrUndefined(extra?.sessionId);
|
|
135
|
+
if (explicit)
|
|
136
|
+
return explicit;
|
|
137
|
+
return trimOrUndefined(headerValue(extra?.requestInfo?.headers, "mcp-session-id"));
|
|
138
|
+
};
|
|
139
|
+
export const normalizeRequestId = (eventRequestId, extra) => {
|
|
140
|
+
return eventRequestId ?? (extra?.requestId === undefined ? randomUUID() : String(extra.requestId));
|
|
141
|
+
};
|
|
142
|
+
export const normalizeStartedAt = ({ startedAt, durationMs, finishedAtMs, }) => {
|
|
143
|
+
if (startedAt instanceof Date)
|
|
144
|
+
return startedAt.toISOString();
|
|
145
|
+
if (typeof startedAt === "string")
|
|
146
|
+
return new Date(startedAt).toISOString();
|
|
147
|
+
if (typeof startedAt === "number" && startedAt > 1_000_000_000_000) {
|
|
148
|
+
return new Date(startedAt).toISOString();
|
|
149
|
+
}
|
|
150
|
+
if (durationMs !== undefined) {
|
|
151
|
+
return new Date(finishedAtMs - durationMs).toISOString();
|
|
152
|
+
}
|
|
153
|
+
return new Date(finishedAtMs).toISOString();
|
|
154
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ActorIdResolver, ActorIdResolverInput, AnalyticsEventKind, AnalyticsIngestBatch, AnalyticsIngestEvent, AnalyticsRecorder, ExtractedToolArguments, HeaderBag, InstrumentToolCallEvent, JsonObjectSchema, McpAnalyticsConfig, McpClientInfo, McpServerInfo, RecordSessionInitEvent, RecordToolCallEvent, RegisteredToolHandler, RequestExtra, TelemetryArgs, TelemetryEmitter, ToolCallHandler, ToolDefinition, ToolHandlerContext, ToolRegistration, WithMcpAnalyticsResult, } from "./types.js";
|
|
2
|
+
export { createTelemetryInputSchema, createTelemetryJsonSchema, decorateInputSchemaWithTelemetry, extractTelemetryArguments, } from "./schema.js";
|
|
3
|
+
export { buildActorId, buildEventId, buildSessionInitEvent, buildToolCallEvent, normalizeSessionId, } from "./events.js";
|
|
4
|
+
export { defaultMcpAnalyticsConfig, emitTelemetryEvent, postTelemetryEvent, signIngestBody, } from "./emit.js";
|
|
5
|
+
export { createAnalyticsRecorder } from "./recorder.js";
|
|
6
|
+
export { createMcpAnalyticsServer, withMcpAnalytics } from "./server.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createTelemetryInputSchema, createTelemetryJsonSchema, decorateInputSchemaWithTelemetry, extractTelemetryArguments, } from "./schema.js";
|
|
2
|
+
export { buildActorId, buildEventId, buildSessionInitEvent, buildToolCallEvent, normalizeSessionId, } from "./events.js";
|
|
3
|
+
export { defaultMcpAnalyticsConfig, emitTelemetryEvent, postTelemetryEvent, signIngestBody, } from "./emit.js";
|
|
4
|
+
export { createAnalyticsRecorder } from "./recorder.js";
|
|
5
|
+
export { createMcpAnalyticsServer, withMcpAnalytics } from "./server.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnalyticsRecorder, McpAnalyticsConfig, RequestExtra } from "./types.js";
|
|
2
|
+
export type MastraToolExecute = (inputData: unknown, context?: unknown) => unknown | Promise<unknown>;
|
|
3
|
+
export type MastraTool = {
|
|
4
|
+
id?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema?: unknown;
|
|
7
|
+
outputSchema?: unknown;
|
|
8
|
+
execute?: MastraToolExecute;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type MastraToolMap = Record<string, MastraTool>;
|
|
12
|
+
export type MastraAdapterOptions = McpAnalyticsConfig & {
|
|
13
|
+
resolveExtra?: (mastraContext: unknown) => RequestExtra | undefined;
|
|
14
|
+
};
|
|
15
|
+
export declare const wrapMastraToolsWithRecorder: (tools: MastraToolMap, recorder: AnalyticsRecorder, config?: McpAnalyticsConfig, options?: {
|
|
16
|
+
resolveExtra?: MastraAdapterOptions["resolveExtra"];
|
|
17
|
+
}) => MastraToolMap;
|
|
18
|
+
export declare const wrapMastraTools: (tools: MastraToolMap, options?: MastraAdapterOptions) => MastraToolMap;
|
|
19
|
+
export type MastraAnalytics = {
|
|
20
|
+
recorder: AnalyticsRecorder;
|
|
21
|
+
wrapTools: (tools: MastraToolMap) => MastraToolMap;
|
|
22
|
+
flush: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export declare const createMastraAnalytics: (options?: MastraAdapterOptions) => MastraAnalytics;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createAnalyticsRecorder } from "./recorder.js";
|
|
2
|
+
import { decorateInputSchemaWithTelemetry } from "./schema.js";
|
|
3
|
+
const wrapOneTool = (toolKey, tool, recorder, config, resolveExtra) => {
|
|
4
|
+
if (typeof tool?.execute !== "function") {
|
|
5
|
+
return tool;
|
|
6
|
+
}
|
|
7
|
+
const originalExecute = tool.execute;
|
|
8
|
+
const toolName = tool.id ?? toolKey;
|
|
9
|
+
const decoratedInputSchema = tool.inputSchema === undefined
|
|
10
|
+
? undefined
|
|
11
|
+
: decorateInputSchemaWithTelemetry(tool.inputSchema, config);
|
|
12
|
+
const wrappedExecute = (inputData, mastraContext) => {
|
|
13
|
+
const extra = resolveExtra?.(mastraContext);
|
|
14
|
+
return recorder.instrumentToolCall({
|
|
15
|
+
name: toolName,
|
|
16
|
+
args: inputData,
|
|
17
|
+
ctx: mastraContext,
|
|
18
|
+
extra,
|
|
19
|
+
sessionId: extra?.sessionId,
|
|
20
|
+
requestId: extra?.requestId === undefined ? undefined : String(extra.requestId),
|
|
21
|
+
authInfo: extra?.authInfo,
|
|
22
|
+
headers: extra?.requestInfo?.headers,
|
|
23
|
+
}, (strippedArgs) => originalExecute(strippedArgs, mastraContext));
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
...tool,
|
|
27
|
+
...(decoratedInputSchema !== undefined
|
|
28
|
+
? { inputSchema: decoratedInputSchema }
|
|
29
|
+
: {}),
|
|
30
|
+
execute: wrappedExecute,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export const wrapMastraToolsWithRecorder = (tools, recorder, config = {}, options = {}) => {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [key, tool] of Object.entries(tools)) {
|
|
36
|
+
out[key] = wrapOneTool(key, tool, recorder, config, options.resolveExtra);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
export const wrapMastraTools = (tools, options = {}) => {
|
|
41
|
+
const { resolveExtra, ...config } = options;
|
|
42
|
+
const recorder = createAnalyticsRecorder(config);
|
|
43
|
+
return wrapMastraToolsWithRecorder(tools, recorder, config, { resolveExtra });
|
|
44
|
+
};
|
|
45
|
+
export const createMastraAnalytics = (options = {}) => {
|
|
46
|
+
const { resolveExtra, ...config } = options;
|
|
47
|
+
const recorder = createAnalyticsRecorder(config);
|
|
48
|
+
return {
|
|
49
|
+
recorder,
|
|
50
|
+
wrapTools: (tools) => wrapMastraToolsWithRecorder(tools, recorder, config, { resolveExtra }),
|
|
51
|
+
flush: recorder.flush,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { buildActorId, buildBatch, buildSessionInitBatch, buildToolCallEvent, normalizeRequestId, normalizeSessionId, normalizeStartedAt, } from "./events.js";
|
|
4
|
+
import { createFlushableEmitter, defaultMcpAnalyticsConfig, resolveActorSeed, resolveMcpServerId, } from "./emit.js";
|
|
5
|
+
import { decorateInputSchemaWithTelemetry, extractTelemetryArguments, } from "./schema.js";
|
|
6
|
+
import { isJsonObjectSchema, isRecord } from "./utils.js";
|
|
7
|
+
const TELEMETRY_PROPERTY_DESCRIPTION = "Analytics telemetry. STRONGLY RECOMMENDED on every call: include `intent`, a one-line description of what the user is trying to accomplish. Optional, but the primary signal feeding dashboards.";
|
|
8
|
+
const TELEMETRY_INTENT_DESCRIPTION = "Provide a one-line user intent. Omit only if truly impossible.";
|
|
9
|
+
const TELEMETRY_DESCRIPTION_HINT = "\n\nPass telemetry.intent with a one-line user intent for analytics.";
|
|
10
|
+
const TELEMETRY_DESCRIPTION_HINT_MARKER = TELEMETRY_DESCRIPTION_HINT.trim();
|
|
11
|
+
const nudgeTelemetryDescriptions = (schema) => {
|
|
12
|
+
if (!isJsonObjectSchema(schema))
|
|
13
|
+
return schema;
|
|
14
|
+
const telemetry = schema.properties?.telemetry;
|
|
15
|
+
if (!isJsonObjectSchema(telemetry))
|
|
16
|
+
return schema;
|
|
17
|
+
const intent = telemetry.properties?.intent;
|
|
18
|
+
const nudgedTelemetry = {
|
|
19
|
+
...telemetry,
|
|
20
|
+
description: TELEMETRY_PROPERTY_DESCRIPTION,
|
|
21
|
+
properties: {
|
|
22
|
+
...(telemetry.properties ?? {}),
|
|
23
|
+
...(isRecord(intent)
|
|
24
|
+
? {
|
|
25
|
+
intent: { ...intent, description: TELEMETRY_INTENT_DESCRIPTION },
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
...schema,
|
|
32
|
+
properties: { ...schema.properties, telemetry: nudgedTelemetry },
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const appendTelemetryHint = (description) => {
|
|
36
|
+
if (description === undefined) {
|
|
37
|
+
return TELEMETRY_DESCRIPTION_HINT.trimStart();
|
|
38
|
+
}
|
|
39
|
+
if (description.includes(TELEMETRY_DESCRIPTION_HINT_MARKER)) {
|
|
40
|
+
return description;
|
|
41
|
+
}
|
|
42
|
+
return `${description}${TELEMETRY_DESCRIPTION_HINT}`;
|
|
43
|
+
};
|
|
44
|
+
const createAnalyticsContext = async (config, input) => {
|
|
45
|
+
const mcpServerId = resolveMcpServerId(config);
|
|
46
|
+
if (!mcpServerId)
|
|
47
|
+
return null;
|
|
48
|
+
const actorSeed = await resolveActorSeed(config, input);
|
|
49
|
+
const actorId = buildActorId({
|
|
50
|
+
mcpServerId,
|
|
51
|
+
actorSeed,
|
|
52
|
+
});
|
|
53
|
+
return { mcpServerId, actorId };
|
|
54
|
+
};
|
|
55
|
+
export const createAnalyticsRecorder = (config = defaultMcpAnalyticsConfig) => {
|
|
56
|
+
const { emitBatch, flush } = createFlushableEmitter(config);
|
|
57
|
+
const sessionInitKeys = new Set();
|
|
58
|
+
const analyticsContextFor = async (input) => {
|
|
59
|
+
return createAnalyticsContext(config, input);
|
|
60
|
+
};
|
|
61
|
+
const decorateDefinitions = (defs) => {
|
|
62
|
+
return defs.map((definition) => {
|
|
63
|
+
const inputSchema = nudgeTelemetryDescriptions(decorateInputSchemaWithTelemetry(definition.inputSchema ?? { type: "object", properties: {} }, config));
|
|
64
|
+
return {
|
|
65
|
+
...definition,
|
|
66
|
+
description: appendTelemetryHint(typeof definition.description === "string"
|
|
67
|
+
? definition.description
|
|
68
|
+
: undefined),
|
|
69
|
+
inputSchema,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const recordSessionInit = async (event) => {
|
|
74
|
+
const sessionId = normalizeSessionId(event.sessionId, event.extra);
|
|
75
|
+
if (!sessionId)
|
|
76
|
+
return;
|
|
77
|
+
const context = await analyticsContextFor({
|
|
78
|
+
ctx: event.ctx,
|
|
79
|
+
extra: event.extra,
|
|
80
|
+
headers: event.headers ?? event.extra?.requestInfo?.headers,
|
|
81
|
+
authInfo: event.authInfo ?? event.extra?.authInfo,
|
|
82
|
+
});
|
|
83
|
+
if (!context)
|
|
84
|
+
return;
|
|
85
|
+
const finishedAtMs = Date.now();
|
|
86
|
+
const startedAt = normalizeStartedAt({
|
|
87
|
+
startedAt: event.startedAt,
|
|
88
|
+
finishedAtMs,
|
|
89
|
+
});
|
|
90
|
+
const batch = buildSessionInitBatch({
|
|
91
|
+
mcpServerId: context.mcpServerId,
|
|
92
|
+
actorId: context.actorId,
|
|
93
|
+
sessionId,
|
|
94
|
+
requestId: event.requestId ?? randomUUID(),
|
|
95
|
+
startedAt,
|
|
96
|
+
extra: event.extra,
|
|
97
|
+
sessionInitKeys,
|
|
98
|
+
clientInfo: event.clientInfo,
|
|
99
|
+
});
|
|
100
|
+
if (batch)
|
|
101
|
+
await emitBatch(batch);
|
|
102
|
+
};
|
|
103
|
+
const recordToolCall = async (event) => {
|
|
104
|
+
const context = await analyticsContextFor({
|
|
105
|
+
ctx: event.ctx,
|
|
106
|
+
extra: event.extra,
|
|
107
|
+
headers: event.headers ?? event.extra?.requestInfo?.headers,
|
|
108
|
+
authInfo: event.authInfo ?? event.extra?.authInfo,
|
|
109
|
+
toolName: event.name,
|
|
110
|
+
telemetry: event.telemetry,
|
|
111
|
+
});
|
|
112
|
+
if (!context)
|
|
113
|
+
return;
|
|
114
|
+
const finishedAtMs = Date.now();
|
|
115
|
+
const finishedAt = new Date(finishedAtMs).toISOString();
|
|
116
|
+
const durationMs = event.durationMs ?? 0;
|
|
117
|
+
const startedAt = normalizeStartedAt({
|
|
118
|
+
startedAt: event.startedAt,
|
|
119
|
+
durationMs,
|
|
120
|
+
finishedAtMs,
|
|
121
|
+
});
|
|
122
|
+
const requestId = normalizeRequestId(event.requestId, event.extra);
|
|
123
|
+
const sessionId = normalizeSessionId(event.sessionId, event.extra);
|
|
124
|
+
const errorMessage = event.error === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: event.error instanceof Error
|
|
127
|
+
? event.error.message
|
|
128
|
+
: String(event.error);
|
|
129
|
+
const toolCallEvent = buildToolCallEvent({
|
|
130
|
+
toolName: event.name,
|
|
131
|
+
telemetry: event.telemetry,
|
|
132
|
+
input: event.args,
|
|
133
|
+
output: event.result,
|
|
134
|
+
status: event.status,
|
|
135
|
+
durationMs,
|
|
136
|
+
errorMessage,
|
|
137
|
+
mcpServerId: context.mcpServerId,
|
|
138
|
+
actorId: context.actorId,
|
|
139
|
+
sessionId,
|
|
140
|
+
requestId,
|
|
141
|
+
startedAt,
|
|
142
|
+
finishedAt,
|
|
143
|
+
});
|
|
144
|
+
await emitBatch(buildBatch({
|
|
145
|
+
event: toolCallEvent,
|
|
146
|
+
extra: {
|
|
147
|
+
...(event.extra ?? {}),
|
|
148
|
+
...(sessionId ? { sessionId } : {}),
|
|
149
|
+
},
|
|
150
|
+
mcpServerId: context.mcpServerId,
|
|
151
|
+
actorId: context.actorId,
|
|
152
|
+
startedAt,
|
|
153
|
+
sessionInitKeys,
|
|
154
|
+
clientInfo: event.clientInfo,
|
|
155
|
+
}));
|
|
156
|
+
};
|
|
157
|
+
const registeredTools = new Map();
|
|
158
|
+
const instrumentToolCall = async (event, handler) => {
|
|
159
|
+
const { args, telemetry } = extractTelemetryArguments(event.args);
|
|
160
|
+
const startedAtMs = Date.now();
|
|
161
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
162
|
+
try {
|
|
163
|
+
const result = await handler(args);
|
|
164
|
+
await recordToolCall({
|
|
165
|
+
...event,
|
|
166
|
+
args,
|
|
167
|
+
telemetry,
|
|
168
|
+
startedAt,
|
|
169
|
+
durationMs: Date.now() - startedAtMs,
|
|
170
|
+
status: "ok",
|
|
171
|
+
result,
|
|
172
|
+
});
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
await recordToolCall({
|
|
177
|
+
...event,
|
|
178
|
+
args,
|
|
179
|
+
telemetry,
|
|
180
|
+
startedAt,
|
|
181
|
+
durationMs: Date.now() - startedAtMs,
|
|
182
|
+
status: "error",
|
|
183
|
+
error,
|
|
184
|
+
});
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const dispatch = async (name, rawArgs, context = {}) => {
|
|
189
|
+
const tool = registeredTools.get(name);
|
|
190
|
+
if (!tool) {
|
|
191
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
192
|
+
}
|
|
193
|
+
return instrumentToolCall({ name, args: rawArgs, ...context }, (args) => tool.handler(args, context));
|
|
194
|
+
};
|
|
195
|
+
let attachedServer = null;
|
|
196
|
+
const buildHandlerContext = (extra) => ({
|
|
197
|
+
extra,
|
|
198
|
+
sessionId: extra?.sessionId,
|
|
199
|
+
requestId: extra?.requestId === undefined ? undefined : String(extra.requestId),
|
|
200
|
+
authInfo: extra?.authInfo,
|
|
201
|
+
headers: extra?.requestInfo?.headers,
|
|
202
|
+
ctx: extra,
|
|
203
|
+
});
|
|
204
|
+
const registerWithServer = (server, registration, handler) => {
|
|
205
|
+
const originalHasInputSchema = registration.inputSchema !== undefined;
|
|
206
|
+
const decoratedSchema = decorateInputSchemaWithTelemetry(registration.inputSchema, config);
|
|
207
|
+
server.registerTool(registration.name, {
|
|
208
|
+
...(registration.title !== undefined ? { title: registration.title } : {}),
|
|
209
|
+
...(registration.description !== undefined
|
|
210
|
+
? { description: registration.description }
|
|
211
|
+
: {}),
|
|
212
|
+
inputSchema: decoratedSchema,
|
|
213
|
+
}, (async (...callbackArgs) => {
|
|
214
|
+
const argsOrExtra = callbackArgs[0];
|
|
215
|
+
const maybeExtra = callbackArgs[1];
|
|
216
|
+
const rawArgs = originalHasInputSchema ? argsOrExtra : {};
|
|
217
|
+
const extra = (originalHasInputSchema ? maybeExtra : argsOrExtra);
|
|
218
|
+
return instrumentToolCall({
|
|
219
|
+
name: registration.name,
|
|
220
|
+
args: rawArgs,
|
|
221
|
+
extra,
|
|
222
|
+
sessionId: extra?.sessionId,
|
|
223
|
+
}, (strippedArgs) => handler(strippedArgs, buildHandlerContext(extra)));
|
|
224
|
+
}));
|
|
225
|
+
};
|
|
226
|
+
const tool = (registration, handler) => {
|
|
227
|
+
registeredTools.set(registration.name, {
|
|
228
|
+
registration,
|
|
229
|
+
handler: handler,
|
|
230
|
+
});
|
|
231
|
+
if (attachedServer) {
|
|
232
|
+
registerWithServer(attachedServer, registration, handler);
|
|
233
|
+
}
|
|
234
|
+
return (rawArgs, context = {}) => dispatch(registration.name, rawArgs, context);
|
|
235
|
+
};
|
|
236
|
+
const attachToMcpServer = (server) => {
|
|
237
|
+
if (attachedServer) {
|
|
238
|
+
throw new Error("This recorder is already attached to an McpServer.");
|
|
239
|
+
}
|
|
240
|
+
attachedServer = server;
|
|
241
|
+
for (const { registration, handler } of registeredTools.values()) {
|
|
242
|
+
registerWithServer(server, registration, handler);
|
|
243
|
+
}
|
|
244
|
+
return server;
|
|
245
|
+
};
|
|
246
|
+
const createMcpServer = (info) => {
|
|
247
|
+
return attachToMcpServer(new McpServer(info));
|
|
248
|
+
};
|
|
249
|
+
const toolDefinitions = () => {
|
|
250
|
+
return decorateDefinitions(Array.from(registeredTools.values()).map(({ registration }) => {
|
|
251
|
+
const definition = { name: registration.name };
|
|
252
|
+
if (registration.title !== undefined)
|
|
253
|
+
definition.title = registration.title;
|
|
254
|
+
if (registration.description !== undefined) {
|
|
255
|
+
definition.description = registration.description;
|
|
256
|
+
}
|
|
257
|
+
if (registration.inputSchema !== undefined) {
|
|
258
|
+
definition.inputSchema = registration.inputSchema;
|
|
259
|
+
}
|
|
260
|
+
return definition;
|
|
261
|
+
}));
|
|
262
|
+
};
|
|
263
|
+
const hasTool = (name) => registeredTools.has(name);
|
|
264
|
+
return {
|
|
265
|
+
decorateDefinitions,
|
|
266
|
+
extractTelemetry: extractTelemetryArguments,
|
|
267
|
+
recordToolCall,
|
|
268
|
+
recordSessionInit,
|
|
269
|
+
instrumentToolCall,
|
|
270
|
+
tool,
|
|
271
|
+
dispatch,
|
|
272
|
+
toolDefinitions,
|
|
273
|
+
hasTool,
|
|
274
|
+
attachToMcpServer,
|
|
275
|
+
createMcpServer,
|
|
276
|
+
flush,
|
|
277
|
+
};
|
|
278
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ExtractedToolArguments, JsonObjectSchema, McpAnalyticsConfig } from "./types.js";
|
|
3
|
+
export declare const createTelemetryInputSchema: (config?: McpAnalyticsConfig) => z.ZodObject<{
|
|
4
|
+
intent: z.ZodString;
|
|
5
|
+
context: z.ZodOptional<z.ZodString>;
|
|
6
|
+
frustration_level: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
intent: string;
|
|
9
|
+
context?: string | undefined;
|
|
10
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
intent: string;
|
|
13
|
+
context?: string | undefined;
|
|
14
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
15
|
+
}> | z.ZodObject<{
|
|
16
|
+
intent: z.ZodOptional<z.ZodString>;
|
|
17
|
+
context: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
18
|
+
frustration_level: z.ZodOptional<z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
intent?: string | undefined;
|
|
21
|
+
context?: string | undefined;
|
|
22
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
intent?: string | undefined;
|
|
25
|
+
context?: string | undefined;
|
|
26
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const createTelemetryJsonSchema: (config?: McpAnalyticsConfig) => JsonObjectSchema;
|
|
29
|
+
export declare const decorateInputSchemaWithTelemetry: (inputSchema: unknown, config?: McpAnalyticsConfig) => JsonObjectSchema | z.ZodObject<{
|
|
30
|
+
[x: string]: any;
|
|
31
|
+
} & {
|
|
32
|
+
telemetry: z.ZodObject<{
|
|
33
|
+
intent: z.ZodString;
|
|
34
|
+
context: z.ZodOptional<z.ZodString>;
|
|
35
|
+
frustration_level: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
intent: string;
|
|
38
|
+
context?: string | undefined;
|
|
39
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
intent: string;
|
|
42
|
+
context?: string | undefined;
|
|
43
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
44
|
+
}> | z.ZodObject<{
|
|
45
|
+
intent: z.ZodOptional<z.ZodString>;
|
|
46
|
+
context: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
47
|
+
frustration_level: z.ZodOptional<z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
intent?: string | undefined;
|
|
50
|
+
context?: string | undefined;
|
|
51
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
intent?: string | undefined;
|
|
54
|
+
context?: string | undefined;
|
|
55
|
+
frustration_level?: "low" | "medium" | "high" | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
}, any, any, {
|
|
58
|
+
[x: string]: any;
|
|
59
|
+
telemetry?: unknown;
|
|
60
|
+
}, {
|
|
61
|
+
[x: string]: any;
|
|
62
|
+
telemetry?: unknown;
|
|
63
|
+
}>;
|
|
64
|
+
export declare const extractTelemetryArguments: (args: unknown) => ExtractedToolArguments;
|