@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/SKILL.md ADDED
@@ -0,0 +1,328 @@
1
+ ---
2
+ name: install-mcp-analytics
3
+ description: >
4
+ Wire the @armature-tech/mcp-analytics SDK into an existing MCP server so tool calls
5
+ emit telemetry to Armature. Use whenever the user wants to add, install, integrate,
6
+ or instrument analytics on an MCP server — e.g. "add Armature analytics to this MCP",
7
+ "instrument my tools", "wire mcp-analytics into our server". Detects which integration
8
+ shape fits the repo (registry-style McpServer, drop-in factory, dispatcher, or Mastra
9
+ MCPServer), makes the edits, and verifies the wiring by checking the schema includes
10
+ the telemetry block and a test tool call produces a signed batch.
11
+ ---
12
+
13
+ # Install @armature-tech/mcp-analytics into an MCP server
14
+
15
+ You are integrating the `@armature-tech/mcp-analytics` SDK into a customer's MCP server
16
+ codebase. The SDK decorates each tool's input schema with a `telemetry.*` block (so the
17
+ agent can pass `intent`, `context`, `frustration_level`), strips those fields before the
18
+ handler runs, and posts a signed batch to Armature after each call.
19
+
20
+ The hard part is picking the right integration shape and not breaking the existing server.
21
+ Four shapes exist; pick one based on how the customer's code looks today.
22
+
23
+ ## Step 1: Identify the integration shape
24
+
25
+ Read enough of the repo to classify it. Grep first; only open files you need.
26
+
27
+ | Signal | Shape |
28
+ | --- | --- |
29
+ | `package.json` depends on `@mastra/mcp` or `@mastra/core`, code calls `new MCPServer({ tools })` | **D. Mastra** |
30
+ | Code calls `new McpServer(...)` and then `server.registerTool(...)` directly | **A. Drop-in** |
31
+ | Code is new / customer wants to define tools through us | **B. Registry-style** |
32
+ | Code hand-rolls `tools/list` and `tools/call` handlers, dispatching by name | **C. Dispatcher** |
33
+
34
+ Mastra (Shape D) check first — it's the only one keyed off a dependency, and `@mastra/mcp`'s
35
+ `MCPServer` looks superficially like the SDK's `McpServer` but is a different surface, so
36
+ Shapes A–C will not fit. A factory that constructs the MCP SDK's `McpServer` and registers
37
+ tools inside is the next most common — that's shape A. Don't ask the user which shape to
38
+ pick; figure it out from the code and announce your choice in one line before editing.
39
+
40
+ If the repo has multiple MCP servers, ask the user which one (use `AskUserQuestion`). Don't
41
+ guess.
42
+
43
+ ## Step 2: Install the dependencies
44
+
45
+ ```sh
46
+ npm install @armature-tech/mcp-analytics
47
+ ```
48
+
49
+ `@modelcontextprotocol/sdk` and `zod` are peer-ish — they should already be in the project.
50
+ If they aren't, install them too. Use the customer's package manager (check for
51
+ `pnpm-lock.yaml` / `yarn.lock` / `bun.lockb` and match it).
52
+
53
+ The package is published to GitHub Packages under the `@armature-tech` scope. If `npm install`
54
+ fails with 404, the project needs a `.npmrc`:
55
+
56
+ ```
57
+ @armature-tech:registry=https://npm.pkg.github.com
58
+ ```
59
+
60
+ Mention this once; don't litter the project with auth instructions.
61
+
62
+ ## Step 3: Add the three environment variables
63
+
64
+ The SDK needs:
65
+
66
+ | Variable | What it is |
67
+ | --- | --- |
68
+ | `ANALYTICS_INGEST_URL` | `https://app.armature.tech/api/mcp-analytics/ingest` for prod |
69
+ | `ANALYTICS_MCP_SERVER_ID` | The MCP server id from the Armature dashboard |
70
+ | `ANALYTICS_INGEST_SECRET` | The shared secret from the Armature dashboard |
71
+
72
+ Add them to whatever env mechanism the project uses (`.env.example`, `wrangler.toml`,
73
+ `vercel.json`, fly secrets, k8s manifests). Do **not** commit real values; put placeholders
74
+ in `.env.example` and tell the user where to paste the real ones.
75
+
76
+ If either `ANALYTICS_MCP_SERVER_ID` or `ANALYTICS_INGEST_SECRET` is missing at runtime,
77
+ the SDK silently no-ops. That's intentional for local dev — say so once, don't add guards.
78
+
79
+ ## Step 4: Pick a delivery mode
80
+
81
+ The default is `delivery: "background"` which schedules the post on `setImmediate`. That
82
+ **will drop batches in serverless** because the function exits before the immediate fires.
83
+
84
+ Use this decision table:
85
+
86
+ | Runtime | `delivery` |
87
+ | --- | --- |
88
+ | Vercel / Lambda / Cloudflare Workers / any per-request serverless | `"await"` |
89
+ | Long-lived Node process, container, fly machine, persistent server | `"background"` + call `recorder.flush()` on `SIGTERM` |
90
+
91
+ Detect the runtime from the repo (look for `vercel.json`, `wrangler.toml`, `Dockerfile`,
92
+ `fly.toml`, `package.json` scripts). If you're not sure, default to `"await"` — it's the
93
+ safe choice and only costs a few ms per call.
94
+
95
+ ## Step 5: Make the edits
96
+
97
+ ### Shape A — Drop-in
98
+
99
+ Wrap the existing server factory. Don't rewrite tool definitions.
100
+
101
+ ```ts
102
+ import { createMcpAnalyticsServer } from "@armature-tech/mcp-analytics";
103
+
104
+ // before:
105
+ // const server = createMyMcpServer();
106
+
107
+ // after:
108
+ const server = createMcpAnalyticsServer(
109
+ () => createMyMcpServer(),
110
+ {
111
+ armature: {
112
+ // endpointUrl / mcpServerId / ingestSecret default to env vars
113
+ delivery: "await", // or "background" — see Step 4
114
+ },
115
+ },
116
+ );
117
+ ```
118
+
119
+ If the factory isn't already a function (e.g. the file does `const server = new McpServer(...)`
120
+ at module top-level and then `server.registerTool(...)` calls follow), refactor it into a
121
+ function first — `createMcpAnalyticsServer` needs to control the call site so the
122
+ `AsyncLocalStorage` context is active when `registerTool` runs. One small refactor:
123
+
124
+ ```ts
125
+ // before
126
+ const server = new McpServer({ name, version });
127
+ server.registerTool("foo", ...);
128
+ server.registerTool("bar", ...);
129
+ export { server };
130
+
131
+ // after
132
+ const createServer = () => {
133
+ const server = new McpServer({ name, version });
134
+ server.registerTool("foo", ...);
135
+ server.registerTool("bar", ...);
136
+ return server;
137
+ };
138
+ export const server = createMcpAnalyticsServer(createServer);
139
+ ```
140
+
141
+ If you need `recorder.flush()` (serverless), use `withMcpAnalytics` instead and call
142
+ `await recorder.flush()` at the end of the request handler. Don't sprinkle `flush()` calls
143
+ through tool handlers — once per request, at the end, is enough.
144
+
145
+ ### Shape B — Registry-style
146
+
147
+ The recorder owns the tool registry. Define tools on the recorder, then ask it to build
148
+ the server.
149
+
150
+ ```ts
151
+ import { createAnalyticsRecorder } from "@armature-tech/mcp-analytics";
152
+ import { z } from "zod";
153
+
154
+ const analytics = createAnalyticsRecorder({
155
+ armature: { delivery: "await" },
156
+ });
157
+
158
+ analytics.tool<{ customer: string }>(
159
+ {
160
+ name: "lookup_customer",
161
+ description: "Look up a customer by name.",
162
+ inputSchema: { customer: z.string().min(1) },
163
+ },
164
+ async (args) => {
165
+ return { content: [{ type: "text", text: await lookup(args.customer) }] };
166
+ },
167
+ );
168
+
169
+ const server = analytics.createMcpServer({ name: "my-mcp", version });
170
+ await server.connect(transport);
171
+ ```
172
+
173
+ Use this only on greenfield code. Don't rewrite an existing server into this shape — use
174
+ Shape A instead.
175
+
176
+ ### Shape C — Dispatcher
177
+
178
+ For servers that publish a JSON-Schema tool catalog and route `tools/call` by name without
179
+ ever touching `McpServer.registerTool`:
180
+
181
+ ```ts
182
+ import { createAnalyticsRecorder } from "@armature-tech/mcp-analytics";
183
+
184
+ const analytics = createAnalyticsRecorder({
185
+ armature: {
186
+ delivery: "await",
187
+ actorId: ({ ctx }) => (ctx as RequestContext).userProfileId,
188
+ },
189
+ });
190
+
191
+ // Register each tool with the recorder — same definitions you had before.
192
+ analytics.tool<{ customer_id: string }>(
193
+ {
194
+ name: "lookup_customer",
195
+ description: "Look up a customer by id.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: { customer_id: { type: "string" } },
199
+ required: ["customer_id"],
200
+ },
201
+ },
202
+ async (args, { ctx }) => db.customers.lookup(args.customer_id, ctx),
203
+ );
204
+
205
+ // In the tools/list handler:
206
+ return { tools: analytics.toolDefinitions() };
207
+
208
+ // In the tools/call handler:
209
+ return await analytics.dispatch(name, rawArgs, { ctx, sessionId });
210
+ ```
211
+
212
+ The `sessionId` should come from whatever you already track per-connection (the MCP
213
+ session id from the `Mcp-Session-Id` header, your own session table, etc.). If the customer
214
+ has no session concept, pass a stable per-connection id — the SDK uses it to fire one
215
+ `session_init` event per new session.
216
+
217
+ ### Shape D — Mastra
218
+
219
+ For servers built on `@mastra/mcp`'s `MCPServer` with tools defined via
220
+ `createTool({...})` from `@mastra/core/tools`. The Mastra adapter operates at the **tool
221
+ level**: it extends each tool's Zod `inputSchema` with the telemetry block, wraps each
222
+ `execute` to strip telemetry from input and emit a batch, and returns a fresh tool map
223
+ you pass straight back into `new MCPServer({ tools })`.
224
+
225
+ ```ts
226
+ import { wrapMastraTools } from "@armature-tech/mcp-analytics/mastra";
227
+
228
+ new MCPServer({
229
+ id: "my-mcp",
230
+ name: "My MCP",
231
+ version: "0.0.1",
232
+ tools: wrapMastraTools(createMyTools(), {
233
+ armature: { delivery: "await" },
234
+ }),
235
+ resources: myResources,
236
+ });
237
+ ```
238
+
239
+ If the server needs `flush()` (long-lived process on `delivery: "background"`) or
240
+ shares a recorder across multiple tool maps, use `createMastraAnalytics`:
241
+
242
+ ```ts
243
+ import { createMastraAnalytics } from "@armature-tech/mcp-analytics/mastra";
244
+
245
+ const analytics = createMastraAnalytics({ armature: { delivery: "background" } });
246
+ new MCPServer({ tools: analytics.wrapTools(createMyTools()), ... });
247
+ process.on("SIGTERM", () => analytics.flush());
248
+ ```
249
+
250
+ To propagate `sessionId` / `authInfo` from Mastra's per-call context, pass a
251
+ `resolveExtra` callback. Mastra's `execute(inputData, context)` second argument is whatever
252
+ the customer's tools already receive — read from it:
253
+
254
+ ```ts
255
+ wrapMastraTools(tools, {
256
+ armature: { delivery: "await" },
257
+ resolveExtra: (mastraContext) => ({
258
+ sessionId: (mastraContext as any)?.runtimeContext?.get?.("sessionId"),
259
+ authInfo: (mastraContext as any)?.requestContext?.authInfo,
260
+ }),
261
+ });
262
+ ```
263
+
264
+ `actorId` is configured the normal way on `config.armature.actorId` — the resolver
265
+ receives `ctx` set to Mastra's second-arg context.
266
+
267
+ The SDK does not import `@mastra/*` at runtime (structural typing), so the adapter
268
+ works with whatever Mastra version the customer is on. Do not add `@mastra/*` to
269
+ their dependencies — it's already there if Shape D applies.
270
+
271
+ ### Recording session_init at handshake (optional, all shapes)
272
+
273
+ By default, `session_init` fires the first time a sessionId shows up in `recordToolCall`.
274
+ If the customer wants the event to fire at MCP handshake time even when the client never
275
+ calls a tool, add this inside the `initialize` JSON-RPC handler:
276
+
277
+ ```ts
278
+ await analytics.recordSessionInit({ sessionId, ctx });
279
+ ```
280
+
281
+ Only mention this if the customer asks about session tracking, or if their MCP server
282
+ already has a custom `initialize` handler — otherwise skip it.
283
+
284
+ ## Step 6: Verify the wiring
285
+
286
+ Two checks. Don't skip them.
287
+
288
+ **Check 1 — Schema includes telemetry.** Spin up the server, ask it for `tools/list`, and
289
+ confirm one of the tools has a `telemetry` property in its `inputSchema`. If the project
290
+ has a dev server script, use it; otherwise write a 10-line script that imports the factory
291
+ and calls `server.tool()` listing. Stop and investigate if the schema isn't decorated —
292
+ that means the `AsyncLocalStorage` context wasn't active when `registerTool` ran (most
293
+ common cause: tools registered outside the factory in Shape A).
294
+
295
+ **Check 2 — A real tool call produces a batch.** Either:
296
+
297
+ - Run a tool against the local mock at `http://127.0.0.1:8787/api/mcp-analytics/ingest`
298
+ (set `ANALYTICS_INGEST_URL` to it and run `npm run dev:armature` if the SDK repo is
299
+ checked out locally), or
300
+ - Set `armature.emit` in the config to a stub that captures the batch, fire a test tool
301
+ call, and assert the captured batch has one `tool_call` event with the right tool name.
302
+
303
+ A passing typecheck is not verification. The schema decoration and the signed batch are
304
+ what matter — verify both.
305
+
306
+ ## Step 7: Mention the gotchas, then stop
307
+
308
+ Tell the user, briefly:
309
+
310
+ - `delivery: "background"` drops batches in serverless. You picked `"await"` (or not — say which).
311
+ - The SDK no-ops silently if env vars are missing. Set them in prod.
312
+ - The package is on GitHub Packages, so CI needs the `.npmrc` line from Step 2 with a `NODE_AUTH_TOKEN`.
313
+
314
+ Don't pad with anything else. End with one line: what you changed and what the user needs
315
+ to do (paste the secrets, deploy).
316
+
317
+ ## What NOT to do
318
+
319
+ - Don't add error handling around `recorder.recordToolCall` — the SDK swallows emit errors
320
+ via `onError`. Wrapping it adds noise.
321
+ - Don't add a `try/catch` around `flush()` either. Pass `onError` in config if the user
322
+ wants custom handling.
323
+ - Don't expose the ingest secret to the client side — it's server-only. If you see it
324
+ imported in a browser bundle path, stop and flag it.
325
+ - Don't rewrite tool definitions to "match the SDK style" if Shape A works. Minimum
326
+ change wins.
327
+ - Don't add a `MCP_ANALYTICS_ENABLED` feature flag. `armature.enabled: false` already
328
+ exists and the SDK no-ops on missing env vars.
@@ -0,0 +1,32 @@
1
+ import type { ActorIdResolverInput, AnalyticsIngestBatch, McpAnalyticsConfig } from "./types.js";
2
+ export declare const defaultMcpAnalyticsConfig: {
3
+ telemetry: {
4
+ intent: "optional";
5
+ };
6
+ armature: {
7
+ endpointUrl: string;
8
+ enabled: true;
9
+ timeoutMs: number;
10
+ };
11
+ };
12
+ export declare const resolveEndpointUrl: (config: McpAnalyticsConfig) => string;
13
+ export declare const resolveIngestSecret: (config: McpAnalyticsConfig) => string | undefined;
14
+ export declare const resolveMcpServerId: (config: McpAnalyticsConfig) => string | undefined;
15
+ export declare const resolveActorSeed: (config: McpAnalyticsConfig, input: ActorIdResolverInput) => Promise<string>;
16
+ export declare const signIngestBody: (body: string, secret: string, timestamp: string) => string;
17
+ export declare const postTelemetryEvent: (batch: AnalyticsIngestBatch, config?: McpAnalyticsConfig) => Promise<{
18
+ skipped: boolean;
19
+ reason: string;
20
+ ok?: undefined;
21
+ status?: undefined;
22
+ } | {
23
+ skipped: boolean;
24
+ ok: boolean;
25
+ status: number;
26
+ reason?: undefined;
27
+ }>;
28
+ export declare const emitTelemetryEvent: (batch: AnalyticsIngestBatch, config?: McpAnalyticsConfig) => Promise<void>;
29
+ export declare const createFlushableEmitter: (config: McpAnalyticsConfig) => {
30
+ emitBatch: (batch: AnalyticsIngestBatch) => Promise<void>;
31
+ flush: () => Promise<void>;
32
+ };
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFlushableEmitter = exports.emitTelemetryEvent = exports.postTelemetryEvent = exports.signIngestBody = exports.resolveActorSeed = exports.resolveMcpServerId = exports.resolveIngestSecret = exports.resolveEndpointUrl = exports.defaultMcpAnalyticsConfig = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const utils_js_1 = require("./utils.js");
6
+ exports.defaultMcpAnalyticsConfig = {
7
+ telemetry: {
8
+ intent: "optional",
9
+ },
10
+ armature: {
11
+ endpointUrl: "http://127.0.0.1:8787/api/mcp-analytics/ingest",
12
+ enabled: true,
13
+ timeoutMs: 4_000,
14
+ },
15
+ };
16
+ const resolveEndpointUrl = (config) => {
17
+ return config.armature?.endpointUrl ??
18
+ (0, utils_js_1.readEnv)("ANALYTICS_INGEST_URL") ??
19
+ exports.defaultMcpAnalyticsConfig.armature.endpointUrl;
20
+ };
21
+ exports.resolveEndpointUrl = resolveEndpointUrl;
22
+ const resolveIngestSecret = (config) => {
23
+ return config.armature?.ingestSecret ?? (0, utils_js_1.readEnv)("ANALYTICS_INGEST_SECRET");
24
+ };
25
+ exports.resolveIngestSecret = resolveIngestSecret;
26
+ const resolveMcpServerId = (config) => {
27
+ return config.armature?.mcpServerId ?? (0, utils_js_1.readEnv)("ANALYTICS_MCP_SERVER_ID");
28
+ };
29
+ exports.resolveMcpServerId = resolveMcpServerId;
30
+ const resolveActorSeed = async (config, input) => {
31
+ const configuredActorId = config.armature?.actorId;
32
+ if (typeof configuredActorId === "function") {
33
+ return configuredActorId(input);
34
+ }
35
+ if (configuredActorId)
36
+ return configuredActorId;
37
+ if (input.authInfo?.token)
38
+ return input.authInfo.token;
39
+ if (input.authInfo?.clientId)
40
+ return input.authInfo.clientId;
41
+ const authorization = (0, utils_js_1.headerValue)(input.headers, "authorization");
42
+ if (authorization)
43
+ return authorization;
44
+ return "anonymous";
45
+ };
46
+ exports.resolveActorSeed = resolveActorSeed;
47
+ const signIngestBody = (body, secret, timestamp) => {
48
+ return (0, node_crypto_1.createHmac)("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
49
+ };
50
+ exports.signIngestBody = signIngestBody;
51
+ const postTelemetryEvent = async (batch, config = exports.defaultMcpAnalyticsConfig) => {
52
+ const endpointUrl = (0, exports.resolveEndpointUrl)(config);
53
+ const ingestSecret = (0, exports.resolveIngestSecret)(config);
54
+ const mcpServerId = (0, exports.resolveMcpServerId)(config);
55
+ if (!ingestSecret || !mcpServerId) {
56
+ return { skipped: true, reason: "ingest_config_missing" };
57
+ }
58
+ const body = JSON.stringify(batch);
59
+ const timestamp = Math.floor(Date.now() / 1000).toString();
60
+ const signature = (0, exports.signIngestBody)(body, ingestSecret, timestamp);
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), config.armature?.timeoutMs ?? exports.defaultMcpAnalyticsConfig.armature.timeoutMs);
63
+ try {
64
+ const response = await fetch(endpointUrl, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ "X-Armature-MCP-Server-Id": mcpServerId,
69
+ "X-Armature-Timestamp": timestamp,
70
+ "X-Armature-Signature": signature,
71
+ },
72
+ body,
73
+ signal: controller.signal,
74
+ });
75
+ if (!response.ok) {
76
+ throw new Error(`Armature ingest failed with ${response.status}: ${await response.text()}`);
77
+ }
78
+ return { skipped: false, ok: true, status: response.status };
79
+ }
80
+ finally {
81
+ clearTimeout(timeout);
82
+ }
83
+ };
84
+ exports.postTelemetryEvent = postTelemetryEvent;
85
+ const reportEmitError = (error, batch, config) => {
86
+ const onError = config.armature?.onError;
87
+ if (onError) {
88
+ onError(error, batch);
89
+ return;
90
+ }
91
+ // eslint-disable-next-line no-console
92
+ console.warn("[mcp-analytics] telemetry emit failed:", error);
93
+ };
94
+ const emitTelemetryEvent = (batch, config = exports.defaultMcpAnalyticsConfig) => {
95
+ if (config.armature?.enabled === false) {
96
+ return Promise.resolve();
97
+ }
98
+ const emit = config.armature?.emit ??
99
+ (async (telemetryBatch) => {
100
+ await (0, exports.postTelemetryEvent)(telemetryBatch, config);
101
+ });
102
+ const run = async () => {
103
+ try {
104
+ await emit(batch);
105
+ }
106
+ catch (error) {
107
+ reportEmitError(error, batch, config);
108
+ }
109
+ };
110
+ if (config.armature?.delivery === "await") {
111
+ return run();
112
+ }
113
+ setImmediate(() => {
114
+ void run();
115
+ });
116
+ return Promise.resolve();
117
+ };
118
+ exports.emitTelemetryEvent = emitTelemetryEvent;
119
+ const createFlushableEmitter = (config) => {
120
+ const pending = new Set();
121
+ const emitBatch = (batch) => {
122
+ if (config.armature?.enabled === false) {
123
+ return Promise.resolve();
124
+ }
125
+ const emit = config.armature?.emit ??
126
+ (async (telemetryBatch) => {
127
+ await (0, exports.postTelemetryEvent)(telemetryBatch, config);
128
+ });
129
+ const run = async () => {
130
+ try {
131
+ await emit(batch);
132
+ }
133
+ catch (error) {
134
+ reportEmitError(error, batch, config);
135
+ }
136
+ };
137
+ if (config.armature?.delivery === "await") {
138
+ return run();
139
+ }
140
+ const task = new Promise((resolve) => {
141
+ setImmediate(resolve);
142
+ })
143
+ .then(run)
144
+ .finally(() => {
145
+ pending.delete(task);
146
+ });
147
+ pending.add(task);
148
+ return Promise.resolve();
149
+ };
150
+ const flush = async () => {
151
+ while (pending.size > 0) {
152
+ await Promise.all(Array.from(pending));
153
+ }
154
+ };
155
+ return { emitBatch, flush };
156
+ };
157
+ exports.createFlushableEmitter = createFlushableEmitter;
@@ -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;