@gomcp/analytics 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/dist/analytics.d.ts +68 -0
  4. package/dist/analytics.d.ts.map +1 -0
  5. package/dist/analytics.js +113 -0
  6. package/dist/analytics.js.map +1 -0
  7. package/dist/collector.d.ts +44 -0
  8. package/dist/collector.d.ts.map +1 -0
  9. package/dist/collector.js +132 -0
  10. package/dist/collector.js.map +1 -0
  11. package/dist/exporters/console.d.ts +6 -0
  12. package/dist/exporters/console.d.ts.map +1 -0
  13. package/dist/exporters/console.js +18 -0
  14. package/dist/exporters/console.js.map +1 -0
  15. package/dist/exporters/custom.d.ts +7 -0
  16. package/dist/exporters/custom.d.ts.map +1 -0
  17. package/dist/exporters/custom.js +15 -0
  18. package/dist/exporters/custom.js.map +1 -0
  19. package/dist/exporters/json.d.ts +6 -0
  20. package/dist/exporters/json.d.ts.map +1 -0
  21. package/dist/exporters/json.js +13 -0
  22. package/dist/exporters/json.js.map +1 -0
  23. package/dist/exporters/otlp.d.ts +9 -0
  24. package/dist/exporters/otlp.d.ts.map +1 -0
  25. package/dist/exporters/otlp.js +83 -0
  26. package/dist/exporters/otlp.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/middleware.d.ts +14 -0
  32. package/dist/middleware.d.ts.map +1 -0
  33. package/dist/middleware.js +184 -0
  34. package/dist/middleware.js.map +1 -0
  35. package/dist/tracing.d.ts +37 -0
  36. package/dist/tracing.d.ts.map +1 -0
  37. package/dist/tracing.js +67 -0
  38. package/dist/tracing.js.map +1 -0
  39. package/dist/types.d.ts +98 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/utils.d.ts +14 -0
  44. package/dist/utils.d.ts.map +1 -0
  45. package/dist/utils.js +44 -0
  46. package/dist/utils.js.map +1 -0
  47. package/package.json +70 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mouaad Aallam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # @gomcp/analytics
2
+
3
+ Lightweight analytics and observability for [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) servers. Zero required dependencies, framework-agnostic, works at the JSON-RPC transport level.
4
+
5
+ ## Features
6
+
7
+ - **Transport-level interception** — works with any MCP server (official SDK, FastMCP, custom)
8
+ - **Handler wrapping** — instrument individual tool handlers for granular control
9
+ - **Multiple exporters** — console, JSON file, OpenTelemetry OTLP, or custom functions
10
+ - **In-memory stats** — p50/p95/p99 latencies, error rates, call counts per tool
11
+ - **Sampling** — configurable sample rate to control overhead
12
+ - **Zero required deps** — only `@modelcontextprotocol/sdk` as a peer dependency
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @gomcp/analytics
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
+ import { McpAnalytics } from "@gomcp/analytics";
26
+
27
+ // 1. Create analytics instance
28
+ const analytics = new McpAnalytics({
29
+ exporter: "console",
30
+ });
31
+
32
+ // 2. Create your server and transport
33
+ const server = new McpServer({ name: "my-server", version: "1.0.0" });
34
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
35
+
36
+ // 3. Instrument the transport (intercepts all tool calls automatically)
37
+ const trackedTransport = analytics.instrument(transport);
38
+ await server.connect(trackedTransport);
39
+
40
+ // 4. Access stats at any time
41
+ console.log(analytics.getStats());
42
+ // { totalCalls: 42, errorRate: 0.02, tools: { search: { count: 30, p50Ms: 120, ... } } }
43
+
44
+ // 5. Clean shutdown
45
+ await analytics.shutdown();
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### `new McpAnalytics(config)`
51
+
52
+ Create an analytics instance.
53
+
54
+ | Option | Type | Default | Description |
55
+ |-------------------|----------------------------------------------------------|---------|-----------------------------------------------------------|
56
+ | `exporter` | `"console" \| "json" \| "otlp" \| Function` | — | Where to send metrics (required) |
57
+ | `json` | `{ path: string }` | — | JSON file config (required when `exporter: "json"`) |
58
+ | `otlp` | `{ endpoint: string, headers?: Record<string, string> }` | — | OTLP config (required when `exporter: "otlp"`) |
59
+ | `sampleRate` | `number` | `1.0` | Fraction of calls to sample (0.0 to 1.0) |
60
+ | `flushIntervalMs` | `number` | `5000` | How often to flush events to the exporter |
61
+ | `maxBufferSize` | `number` | `10000` | Max events in the ring buffer |
62
+ | `metadata` | `Record<string, string>` | — | Metadata added to every event |
63
+ | `tracing` | `boolean` | `false` | Create OpenTelemetry spans via the global tracer provider |
64
+
65
+ ### `analytics.instrument(transport)`
66
+
67
+ Wrap an MCP transport to automatically intercept all `tools/call` requests and responses. Returns a proxy transport that can be used in place of the original.
68
+
69
+ ```typescript
70
+ const trackedTransport = analytics.instrument(transport);
71
+ await server.connect(trackedTransport);
72
+ ```
73
+
74
+ ### `analytics.track(handler, toolName?)`
75
+
76
+ Wrap a tool handler function to record metrics. Use this when you want per-handler control instead of transport-level interception.
77
+
78
+ ```typescript
79
+ server.tool("search", schema, analytics.track(async (params) => {
80
+ return await doSearch(params);
81
+ }, "search"));
82
+ ```
83
+
84
+ ### `analytics.getStats()`
85
+
86
+ Returns an `AnalyticsSnapshot` with aggregated metrics:
87
+
88
+ ```typescript
89
+ interface AnalyticsSnapshot {
90
+ totalCalls: number;
91
+ totalErrors: number;
92
+ errorRate: number;
93
+ uptimeMs: number;
94
+ tools: Record<string, ToolStats>;
95
+ }
96
+
97
+ interface ToolStats {
98
+ count: number;
99
+ errorCount: number;
100
+ errorRate: number;
101
+ p50Ms: number;
102
+ p95Ms: number;
103
+ p99Ms: number;
104
+ avgMs: number;
105
+ lastCalledAt: number; // Unix timestamp ms
106
+ }
107
+ ```
108
+
109
+ ### `analytics.getToolStats(toolName)`
110
+
111
+ Get stats for a specific tool. Returns `undefined` if the tool hasn't been called.
112
+
113
+ ### `analytics.flush()`
114
+
115
+ Force-flush all pending events to the exporter.
116
+
117
+ ### `analytics.reset()`
118
+
119
+ Clear all collected data.
120
+
121
+ ### `analytics.shutdown()`
122
+
123
+ Stop the flush timer and flush remaining events. Call this on process exit.
124
+
125
+ ## Exporters
126
+
127
+ ### Console
128
+
129
+ Pretty-prints batches to stdout:
130
+
131
+ ```typescript
132
+ new McpAnalytics({ exporter: "console" });
133
+ ```
134
+
135
+ ### JSON File
136
+
137
+ Appends events as JSONL (one JSON object per line):
138
+
139
+ ```typescript
140
+ new McpAnalytics({
141
+ exporter: "json",
142
+ json: { path: "./analytics.jsonl" },
143
+ });
144
+ ```
145
+
146
+ ### OpenTelemetry OTLP
147
+
148
+ Sends events as OpenTelemetry spans. Requires `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`, and `@opentelemetry/exporter-trace-otlp-http` as peer dependencies (dynamically imported only when used):
149
+
150
+ ```bash
151
+ npm install @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http
152
+ ```
153
+
154
+ ```typescript
155
+ new McpAnalytics({
156
+ exporter: "otlp",
157
+ otlp: {
158
+ endpoint: "http://localhost:4318/v1/traces",
159
+ headers: { "Authorization": "Bearer ..." },
160
+ },
161
+ });
162
+ ```
163
+
164
+ ### Custom Function
165
+
166
+ Provide your own export function:
167
+
168
+ ```typescript
169
+ new McpAnalytics({
170
+ exporter: async (events) => {
171
+ await fetch("https://my-analytics.example.com/ingest", {
172
+ method: "POST",
173
+ body: JSON.stringify(events),
174
+ });
175
+ },
176
+ });
177
+ ```
178
+
179
+ ## Tracing (dd-trace / OpenTelemetry)
180
+
181
+ When you use an APM like [dd-trace](https://github.com/DataDog/dd-trace-js) that registers itself as the global OpenTelemetry provider, you can make MCP tool calls appear as spans in your existing traces with zero extra configuration:
182
+
183
+ ```typescript
184
+ import "dd-trace/init"; // sets up dd-trace as global OTel provider
185
+
186
+ import { McpAnalytics } from "@gomcp/analytics";
187
+
188
+ const analytics = new McpAnalytics({
189
+ exporter: "console",
190
+ tracing: true, // creates spans via the global tracer provider
191
+ });
192
+
193
+ const tracked = analytics.instrument(transport);
194
+ await server.connect(tracked);
195
+ // Tool calls now appear as "mcp.tool_call" spans in Datadog
196
+ ```
197
+
198
+ This works with any OTel-compatible provider (Datadog, New Relic, Honeycomb, etc.). The `tracing` flag dynamically imports `@opentelemetry/api` and uses the global tracer — no OTLP exporter setup needed.
199
+
200
+ When using `analytics.track()` (handler wrapping), the handler executes inside the span context, so any downstream OTel-instrumented calls (HTTP, DB, etc.) become children of the MCP tool span.
201
+
202
+ ### OTLP exporter with global provider
203
+
204
+ If you're already using the OTLP exporter and want it to send spans through your global provider instead of creating an isolated one:
205
+
206
+ ```typescript
207
+ new McpAnalytics({
208
+ exporter: "otlp",
209
+ otlp: {
210
+ endpoint: "unused-when-global", // ignored when useGlobalProvider is true
211
+ useGlobalProvider: true,
212
+ },
213
+ });
214
+ ```
215
+
216
+ ### Span attributes
217
+
218
+ Each `mcp.tool_call` span includes these attributes:
219
+
220
+ | Attribute | Description |
221
+ |--------------------------|-------------------------------------------------|
222
+ | `mcp.tool.name` | Tool name |
223
+ | `mcp.tool.input_size` | Input size in bytes |
224
+ | `mcp.tool.duration_ms` | Duration (OTLP exporter only) |
225
+ | `mcp.tool.success` | Whether the call succeeded (OTLP exporter only) |
226
+ | `mcp.tool.output_size` | Output size in bytes (OTLP exporter only) |
227
+ | `mcp.tool.error_message` | Error message if failed (OTLP exporter only) |
228
+
229
+ ## License
230
+
231
+ MIT
@@ -0,0 +1,68 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import type { AnalyticsConfig, AnalyticsSnapshot, InstrumentedTransport, ToolStats } from "./types.js";
3
+ /**
4
+ * MCP Analytics — lightweight observability for MCP servers.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const analytics = new McpAnalytics({ exporter: "console" });
9
+ *
10
+ * // Instrument a transport
11
+ * const tracked = analytics.instrument(transport);
12
+ * await server.connect(tracked);
13
+ *
14
+ * // Or wrap individual handlers
15
+ * server.tool("search", schema, analytics.track(handler));
16
+ *
17
+ * // Get stats
18
+ * console.log(analytics.getStats());
19
+ *
20
+ * // Shutdown
21
+ * await analytics.flush();
22
+ * ```
23
+ */
24
+ export declare class McpAnalytics {
25
+ private readonly collector;
26
+ private readonly sampleRate;
27
+ private readonly metadata?;
28
+ private readonly tracing;
29
+ constructor(config: AnalyticsConfig);
30
+ /**
31
+ * Instrument an MCP transport to automatically track all tool calls.
32
+ * Returns proxy transport that can be used in place of the original.
33
+ */
34
+ instrument(transport: Transport): InstrumentedTransport;
35
+ /**
36
+ * Wrap a tool handler function to track its execution.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * server.tool("search", schema, analytics.track(async (params) => {
41
+ * return await doSearch(params);
42
+ * }, "search"));
43
+ * ```
44
+ */
45
+ track<TArgs extends unknown[], TResult>(handler: (...args: TArgs) => TResult | Promise<TResult>, toolName?: string): (...args: TArgs) => Promise<TResult>;
46
+ /**
47
+ * Get a snapshot of all analytics data.
48
+ */
49
+ getStats(): AnalyticsSnapshot;
50
+ /**
51
+ * Get stats for a specific tool.
52
+ */
53
+ getToolStats(toolName: string): ToolStats | undefined;
54
+ /**
55
+ * Flush all pending events to the exporter.
56
+ */
57
+ flush(): Promise<void>;
58
+ /**
59
+ * Reset all collected data.
60
+ */
61
+ reset(): void;
62
+ /**
63
+ * Stop the analytics instance (clears flush timer and flushes remaining events).
64
+ */
65
+ shutdown(): Promise<void>;
66
+ private resolveExporter;
67
+ }
68
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAQ/E,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAEjB,qBAAqB,EACrB,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,MAAM,EAAE,eAAe;IAanC;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,SAAS,GAAG,qBAAqB;IAUvD;;;;;;;;;OASG;IACH,KAAK,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACpC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EACvD,QAAQ,CAAC,EAAE,MAAM,GAChB,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC;IAYvC;;OAEG;IACH,QAAQ,IAAI,iBAAiB;IAI7B;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAIrD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,OAAO,CAAC,eAAe;CA0BxB"}
@@ -0,0 +1,113 @@
1
+ import { Collector } from "./collector.js";
2
+ import { createConsoleExporter } from "./exporters/console.js";
3
+ import { createCustomExporter } from "./exporters/custom.js";
4
+ import { createJsonExporter } from "./exporters/json.js";
5
+ import { createOtlpExporter } from "./exporters/otlp.js";
6
+ import { instrumentTransport, wrapToolHandler } from "./middleware.js";
7
+ /**
8
+ * MCP Analytics — lightweight observability for MCP servers.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const analytics = new McpAnalytics({ exporter: "console" });
13
+ *
14
+ * // Instrument a transport
15
+ * const tracked = analytics.instrument(transport);
16
+ * await server.connect(tracked);
17
+ *
18
+ * // Or wrap individual handlers
19
+ * server.tool("search", schema, analytics.track(handler));
20
+ *
21
+ * // Get stats
22
+ * console.log(analytics.getStats());
23
+ *
24
+ * // Shutdown
25
+ * await analytics.flush();
26
+ * ```
27
+ */
28
+ export class McpAnalytics {
29
+ collector;
30
+ sampleRate;
31
+ metadata;
32
+ tracing;
33
+ constructor(config) {
34
+ this.sampleRate = config.sampleRate ?? 1;
35
+ this.metadata = config.metadata;
36
+ this.tracing = config.tracing ?? false;
37
+ const exporter = this.resolveExporter(config);
38
+ this.collector = new Collector(config.maxBufferSize ?? 10_000, exporter, config.flushIntervalMs ?? 5_000);
39
+ }
40
+ /**
41
+ * Instrument an MCP transport to automatically track all tool calls.
42
+ * Returns proxy transport that can be used in place of the original.
43
+ */
44
+ instrument(transport) {
45
+ return instrumentTransport(transport, this.collector, this.sampleRate, this.metadata, this.tracing);
46
+ }
47
+ /**
48
+ * Wrap a tool handler function to track its execution.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * server.tool("search", schema, analytics.track(async (params) => {
53
+ * return await doSearch(params);
54
+ * }, "search"));
55
+ * ```
56
+ */
57
+ track(handler, toolName) {
58
+ const name = toolName ?? (handler.name || "anonymous");
59
+ return wrapToolHandler(name, handler, this.collector, this.sampleRate, this.metadata, this.tracing);
60
+ }
61
+ /**
62
+ * Get a snapshot of all analytics data.
63
+ */
64
+ getStats() {
65
+ return this.collector.getStats();
66
+ }
67
+ /**
68
+ * Get stats for a specific tool.
69
+ */
70
+ getToolStats(toolName) {
71
+ return this.collector.getToolStats(toolName);
72
+ }
73
+ /**
74
+ * Flush all pending events to the exporter.
75
+ */
76
+ async flush() {
77
+ await this.collector.flush();
78
+ }
79
+ /**
80
+ * Reset all collected data.
81
+ */
82
+ reset() {
83
+ this.collector.reset();
84
+ }
85
+ /**
86
+ * Stop the analytics instance (clears flush timer and flushes remaining events).
87
+ */
88
+ async shutdown() {
89
+ await this.collector.destroy();
90
+ }
91
+ resolveExporter(config) {
92
+ if (typeof config.exporter === "function") {
93
+ return createCustomExporter(config.exporter);
94
+ }
95
+ switch (config.exporter) {
96
+ case "console":
97
+ return createConsoleExporter();
98
+ case "json": {
99
+ if (!config.json) {
100
+ throw new Error('McpAnalytics: "json" exporter requires a "json" config with "path"');
101
+ }
102
+ return createJsonExporter(config.json);
103
+ }
104
+ case "otlp": {
105
+ if (!config.otlp) {
106
+ throw new Error('McpAnalytics: "otlp" exporter requires an "otlp" config with "endpoint"');
107
+ }
108
+ return createOtlpExporter(config.otlp);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { AnalyticsSnapshot, ExporterFn, ToolCallEvent, ToolStats } from "./types.js";
2
+ /**
3
+ * In-memory ring buffer that collects ToolCallEvents, computes stats,
4
+ * and periodically flushes to an exporter.
5
+ */
6
+ export declare class Collector {
7
+ private readonly maxBufferSize;
8
+ private readonly exporter;
9
+ private readonly buffer;
10
+ private readonly accumulators;
11
+ private totalCalls;
12
+ private totalErrors;
13
+ private readonly startTime;
14
+ private flushTimer;
15
+ /** Events accumulated since last flush, to be sent to the exporter */
16
+ private pending;
17
+ constructor(maxBufferSize: number, exporter: ExporterFn, flushIntervalMs: number);
18
+ /**
19
+ * Record a new tool call event.
20
+ */
21
+ record(event: ToolCallEvent): void;
22
+ /**
23
+ * Get aggregated stats for all tools.
24
+ */
25
+ getStats(): AnalyticsSnapshot;
26
+ /**
27
+ * Get stats for a single tool.
28
+ */
29
+ getToolStats(toolName: string): ToolStats | undefined;
30
+ /**
31
+ * Flush pending events to the exporter.
32
+ */
33
+ flush(): Promise<void>;
34
+ /**
35
+ * Reset all collected data.
36
+ */
37
+ reset(): void;
38
+ /**
39
+ * Stop the flush timer and flush remaining events.
40
+ */
41
+ destroy(): Promise<void>;
42
+ private accToStats;
43
+ }
44
+ //# sourceMappingURL=collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,SAAS,EACV,MAAM,YAAY,CAAC;AAepB;;;GAGG;AACH,qBAAa,SAAS;IAYlB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAZ3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IAExC,OAAO,CAAC,UAAU,CAA6C;IAC/D,sEAAsE;IACtE,OAAO,CAAC,OAAO,CAAuB;gBAGnB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,UAAU,EACrC,eAAe,EAAE,MAAM;IAiBzB;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA8BlC;;OAEG;IACH,QAAQ,IAAI,iBAAiB;IAc7B;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAMrD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B,OAAO,CAAC,UAAU;CAYnB"}
@@ -0,0 +1,132 @@
1
+ import { percentile, sortedInsert } from "./utils.js";
2
+ /**
3
+ * In-memory ring buffer that collects ToolCallEvents, computes stats,
4
+ * and periodically flushes to an exporter.
5
+ */
6
+ export class Collector {
7
+ maxBufferSize;
8
+ exporter;
9
+ buffer = [];
10
+ accumulators = new Map();
11
+ totalCalls = 0;
12
+ totalErrors = 0;
13
+ startTime = Date.now();
14
+ flushTimer;
15
+ /** Events accumulated since last flush, to be sent to the exporter */
16
+ pending = [];
17
+ constructor(maxBufferSize, exporter, flushIntervalMs) {
18
+ this.maxBufferSize = maxBufferSize;
19
+ this.exporter = exporter;
20
+ if (flushIntervalMs > 0) {
21
+ this.flushTimer = setInterval(() => {
22
+ void this.flush();
23
+ }, flushIntervalMs);
24
+ // Don't hold the process open for analytics flushing
25
+ if (this.flushTimer &&
26
+ typeof this.flushTimer === "object" &&
27
+ "unref" in this.flushTimer) {
28
+ this.flushTimer.unref();
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Record a new tool call event.
34
+ */
35
+ record(event) {
36
+ // Ring buffer: drop oldest when full
37
+ if (this.buffer.length >= this.maxBufferSize) {
38
+ this.buffer.shift();
39
+ }
40
+ this.buffer.push(event);
41
+ this.pending.push(event);
42
+ this.totalCalls++;
43
+ if (!event.success)
44
+ this.totalErrors++;
45
+ // Update per-tool accumulator
46
+ let acc = this.accumulators.get(event.toolName);
47
+ if (!acc) {
48
+ acc = {
49
+ count: 0,
50
+ errorCount: 0,
51
+ totalMs: 0,
52
+ durations: [],
53
+ lastCalledAt: 0,
54
+ };
55
+ this.accumulators.set(event.toolName, acc);
56
+ }
57
+ acc.count++;
58
+ if (!event.success)
59
+ acc.errorCount++;
60
+ acc.totalMs += event.durationMs;
61
+ sortedInsert(acc.durations, event.durationMs);
62
+ acc.lastCalledAt = event.timestamp;
63
+ }
64
+ /**
65
+ * Get aggregated stats for all tools.
66
+ */
67
+ getStats() {
68
+ const tools = {};
69
+ for (const [name, acc] of this.accumulators) {
70
+ tools[name] = this.accToStats(acc);
71
+ }
72
+ return {
73
+ totalCalls: this.totalCalls,
74
+ totalErrors: this.totalErrors,
75
+ errorRate: this.totalCalls > 0 ? this.totalErrors / this.totalCalls : 0,
76
+ uptimeMs: Date.now() - this.startTime,
77
+ tools,
78
+ };
79
+ }
80
+ /**
81
+ * Get stats for a single tool.
82
+ */
83
+ getToolStats(toolName) {
84
+ const acc = this.accumulators.get(toolName);
85
+ if (!acc)
86
+ return undefined;
87
+ return this.accToStats(acc);
88
+ }
89
+ /**
90
+ * Flush pending events to the exporter.
91
+ */
92
+ async flush() {
93
+ if (this.pending.length === 0)
94
+ return;
95
+ const batch = this.pending;
96
+ this.pending = [];
97
+ await this.exporter(batch);
98
+ }
99
+ /**
100
+ * Reset all collected data.
101
+ */
102
+ reset() {
103
+ this.buffer.length = 0;
104
+ this.pending.length = 0;
105
+ this.accumulators.clear();
106
+ this.totalCalls = 0;
107
+ this.totalErrors = 0;
108
+ }
109
+ /**
110
+ * Stop the flush timer and flush remaining events.
111
+ */
112
+ async destroy() {
113
+ if (this.flushTimer) {
114
+ clearInterval(this.flushTimer);
115
+ this.flushTimer = undefined;
116
+ }
117
+ await this.flush();
118
+ }
119
+ accToStats(acc) {
120
+ return {
121
+ count: acc.count,
122
+ errorCount: acc.errorCount,
123
+ errorRate: acc.count > 0 ? acc.errorCount / acc.count : 0,
124
+ p50Ms: percentile(acc.durations, 50),
125
+ p95Ms: percentile(acc.durations, 95),
126
+ p99Ms: percentile(acc.durations, 99),
127
+ avgMs: acc.count > 0 ? acc.totalMs / acc.count : 0,
128
+ lastCalledAt: acc.lastCalledAt,
129
+ };
130
+ }
131
+ }
132
+ //# sourceMappingURL=collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.js","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ToolCallEvent } from "../types.js";
2
+ /**
3
+ * Console exporter: pretty-prints each batch of events to stdout.
4
+ */
5
+ export declare function createConsoleExporter(): (events: ToolCallEvent[]) => Promise<void>;
6
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../src/exporters/console.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,CACvC,MAAM,EAAE,aAAa,EAAE,KACpB,OAAO,CAAC,IAAI,CAAC,CAiBjB"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Console exporter: pretty-prints each batch of events to stdout.
3
+ */
4
+ export function createConsoleExporter() {
5
+ return async (events) => {
6
+ if (events.length === 0)
7
+ return;
8
+ const lines = ["[McpAnalytics] Flushing batch:"];
9
+ for (const e of events) {
10
+ const errorSuffix = e.errorCode ? ` (${e.errorCode})` : "";
11
+ const status = e.success ? "OK" : `ERR${errorSuffix}`;
12
+ const meta = e.sessionId ? ` session=${e.sessionId}` : "";
13
+ lines.push(` ${e.toolName} ${status} ${e.durationMs}ms in=${e.inputSize}B out=${e.outputSize}B${meta}`);
14
+ }
15
+ console.log(lines.join("\n"));
16
+ };
17
+ }
18
+ //# sourceMappingURL=console.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.js","sourceRoot":"","sources":["../../src/exporters/console.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,qBAAqB;IAGnC,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEhC,MAAM,KAAK,GAAa,CAAC,gCAAgC,CAAC,CAAC;QAE3D,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,WAAW,EAAE,CAAC;YACtD,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,KAAK,CAAC,IAAI,CACR,KAAK,CAAC,CAAC,QAAQ,IAAI,MAAM,IAAI,CAAC,CAAC,UAAU,SAAS,CAAC,CAAC,SAAS,SAAS,CAAC,CAAC,UAAU,IAAI,IAAI,EAAE,CAC7F,CAAC;QACJ,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { ExporterFn, ToolCallEvent } from "../types.js";
2
+ /**
3
+ * Wraps a user-provided export function, catching errors to prevent
4
+ * exporter failures from disrupting the MCP server.
5
+ */
6
+ export declare function createCustomExporter(fn: ExporterFn): (events: ToolCallEvent[]) => Promise<void>;
7
+ //# sourceMappingURL=custom.d.ts.map