@mcploom/analytics 0.2.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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/index.cjs +685 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +199 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +685 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -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,258 @@
|
|
|
1
|
+
# @mcploom/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
|
+
- **Session analytics** — aggregated metrics per `sessionId` with top-session ranking
|
|
12
|
+
- **Sampling** — configurable sample rate to control overhead
|
|
13
|
+
- **Bounded percentile memory** — keeps a fixed recent latency window per accumulator
|
|
14
|
+
- **Zero required deps** — only `@modelcontextprotocol/sdk` as a peer dependency
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mcploom/analytics
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
27
|
+
import { McpAnalytics } from "@mcploom/analytics";
|
|
28
|
+
|
|
29
|
+
// 1. Create analytics instance
|
|
30
|
+
const analytics = new McpAnalytics({
|
|
31
|
+
exporter: "console",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. Create your server and transport
|
|
35
|
+
const server = new McpServer({ name: "my-server", version: "1.0.0" });
|
|
36
|
+
const transport = new StreamableHTTPServerTransport({
|
|
37
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 3. Instrument the transport (intercepts all tool calls automatically)
|
|
41
|
+
const trackedTransport = analytics.instrument(transport);
|
|
42
|
+
await server.connect(trackedTransport);
|
|
43
|
+
|
|
44
|
+
// 4. Access stats at any time
|
|
45
|
+
console.log(analytics.getStats());
|
|
46
|
+
// { totalCalls: 42, errorRate: 0.02, tools: { search: { count: 30, p50Ms: 120, ... } } }
|
|
47
|
+
|
|
48
|
+
// 5. Clean shutdown
|
|
49
|
+
await analytics.shutdown();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `new McpAnalytics(config)`
|
|
55
|
+
|
|
56
|
+
Create an analytics instance.
|
|
57
|
+
|
|
58
|
+
| Option | Type | Default | Description |
|
|
59
|
+
| ------------------ | -------------------------------------------------------- | ------------ | --------------------------------------------------------- |
|
|
60
|
+
| `exporter` | `"console" \| "json" \| "otlp" \| Function` | — | Where to send metrics (required) |
|
|
61
|
+
| `json` | `{ path: string }` | — | JSON file config (required when `exporter: "json"`) |
|
|
62
|
+
| `otlp` | `{ endpoint: string, headers?: Record<string, string> }` | — | OTLP config (required when `exporter: "otlp"`) |
|
|
63
|
+
| `sampleRate` | `number` | `1.0` | Fraction of calls to sample (0.0 to 1.0) |
|
|
64
|
+
| `flushIntervalMs` | `number` | `5000` | How often to flush events to the exporter |
|
|
65
|
+
| `maxBufferSize` | `number` | `10000` | Max events in the ring buffer |
|
|
66
|
+
| `metadata` | `Record<string, string>` | — | Metadata added to every event |
|
|
67
|
+
| `samplingStrategy` | `"per_call" \| "per_session"` | `"per_call"` | Sampling behavior for transport instrumentation |
|
|
68
|
+
| `toolWindowSize` | `number` | `2048` | Recent durations kept per accumulator for percentiles |
|
|
69
|
+
| `tracing` | `boolean` | `false` | Create OpenTelemetry spans via the global tracer provider |
|
|
70
|
+
|
|
71
|
+
### `analytics.instrument(transport)`
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const trackedTransport = analytics.instrument(transport);
|
|
77
|
+
await server.connect(trackedTransport);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `analytics.track(handler, toolName?)`
|
|
81
|
+
|
|
82
|
+
Wrap a tool handler function to record metrics. Use this when you want per-handler control instead of transport-level interception.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
server.tool(
|
|
86
|
+
"search",
|
|
87
|
+
schema,
|
|
88
|
+
analytics.track(async (params) => {
|
|
89
|
+
return await doSearch(params);
|
|
90
|
+
}, "search"),
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `analytics.getStats()`
|
|
95
|
+
|
|
96
|
+
Returns an `AnalyticsSnapshot` with aggregated metrics:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
interface AnalyticsSnapshot {
|
|
100
|
+
totalCalls: number;
|
|
101
|
+
totalErrors: number;
|
|
102
|
+
errorRate: number;
|
|
103
|
+
uptimeMs: number;
|
|
104
|
+
tools: Record<string, ToolStats>;
|
|
105
|
+
sessions: Record<string, SessionStats>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface ToolStats {
|
|
109
|
+
count: number;
|
|
110
|
+
errorCount: number;
|
|
111
|
+
errorRate: number;
|
|
112
|
+
p50Ms: number;
|
|
113
|
+
p95Ms: number;
|
|
114
|
+
p99Ms: number;
|
|
115
|
+
avgMs: number;
|
|
116
|
+
lastCalledAt: number; // Unix timestamp ms
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface SessionStats {
|
|
120
|
+
count: number;
|
|
121
|
+
errorCount: number;
|
|
122
|
+
errorRate: number;
|
|
123
|
+
avgMs: number;
|
|
124
|
+
lastCalledAt: number; // Unix timestamp ms
|
|
125
|
+
tools: Record<string, ToolStats>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### `analytics.getToolStats(toolName)`
|
|
130
|
+
|
|
131
|
+
Get stats for a specific tool. Returns `undefined` if the tool hasn't been called.
|
|
132
|
+
|
|
133
|
+
### `analytics.getSessionStats(sessionId)`
|
|
134
|
+
|
|
135
|
+
Get stats for a specific session. Returns `undefined` if the session hasn't been observed.
|
|
136
|
+
|
|
137
|
+
### `analytics.getTopSessions(limit?)`
|
|
138
|
+
|
|
139
|
+
Returns sessions ordered by call count (descending). Default `limit` is `10`.
|
|
140
|
+
|
|
141
|
+
### `analytics.flush()`
|
|
142
|
+
|
|
143
|
+
Force-flush all pending events to the exporter.
|
|
144
|
+
|
|
145
|
+
### `analytics.reset()`
|
|
146
|
+
|
|
147
|
+
Clear all collected data.
|
|
148
|
+
|
|
149
|
+
### `analytics.shutdown()`
|
|
150
|
+
|
|
151
|
+
Stop the flush timer and flush remaining events. Call this on process exit.
|
|
152
|
+
|
|
153
|
+
## Reliability Semantics
|
|
154
|
+
|
|
155
|
+
- Flush failures do not drop events. Failed batches are re-queued and retried on the next flush.
|
|
156
|
+
- Periodic flush errors are handled internally (they are reported, but they do not crash the process).
|
|
157
|
+
- Percentile memory is bounded via `toolWindowSize` (recent-window percentile calculation).
|
|
158
|
+
|
|
159
|
+
## Migration Note
|
|
160
|
+
|
|
161
|
+
- `otlp.useGlobalProvider` has been removed. The OTLP exporter now always uses its own OTLP provider.
|
|
162
|
+
If you want spans in your app's global tracer context, use `tracing: true`.
|
|
163
|
+
|
|
164
|
+
## Exporters
|
|
165
|
+
|
|
166
|
+
### Console
|
|
167
|
+
|
|
168
|
+
Pretty-prints batches to stdout:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
new McpAnalytics({ exporter: "console" });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### JSON File
|
|
175
|
+
|
|
176
|
+
Appends events as JSONL (one JSON object per line):
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
new McpAnalytics({
|
|
180
|
+
exporter: "json",
|
|
181
|
+
json: { path: "./analytics.jsonl" },
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### OpenTelemetry OTLP
|
|
186
|
+
|
|
187
|
+
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):
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm install @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
new McpAnalytics({
|
|
195
|
+
exporter: "otlp",
|
|
196
|
+
otlp: {
|
|
197
|
+
endpoint: "http://localhost:4318/v1/traces",
|
|
198
|
+
headers: { Authorization: "Bearer ..." },
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Note: OTLP export emits synthetic spans derived from collected tool-call events.
|
|
204
|
+
|
|
205
|
+
### Custom Function
|
|
206
|
+
|
|
207
|
+
Provide your own export function:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
new McpAnalytics({
|
|
211
|
+
exporter: async (events) => {
|
|
212
|
+
await fetch("https://my-analytics.example.com/ingest", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
body: JSON.stringify(events),
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Tracing (dd-trace / OpenTelemetry)
|
|
221
|
+
|
|
222
|
+
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:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import "dd-trace/init"; // sets up dd-trace as global OTel provider
|
|
226
|
+
|
|
227
|
+
import { McpAnalytics } from "@mcploom/analytics";
|
|
228
|
+
|
|
229
|
+
const analytics = new McpAnalytics({
|
|
230
|
+
exporter: "console",
|
|
231
|
+
tracing: true, // creates spans via the global tracer provider
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const tracked = analytics.instrument(transport);
|
|
235
|
+
await server.connect(tracked);
|
|
236
|
+
// Tool calls now appear as "mcp.tool_call" spans in Datadog
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
241
|
+
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.
|
|
242
|
+
|
|
243
|
+
### Span attributes
|
|
244
|
+
|
|
245
|
+
Each `mcp.tool_call` span includes these attributes:
|
|
246
|
+
|
|
247
|
+
| Attribute | Description |
|
|
248
|
+
| ------------------------ | ----------------------------------------------- |
|
|
249
|
+
| `mcp.tool.name` | Tool name |
|
|
250
|
+
| `mcp.tool.input_size` | Input size in bytes |
|
|
251
|
+
| `mcp.tool.duration_ms` | Duration (OTLP exporter only) |
|
|
252
|
+
| `mcp.tool.success` | Whether the call succeeded (OTLP exporter only) |
|
|
253
|
+
| `mcp.tool.output_size` | Output size in bytes (OTLP exporter only) |
|
|
254
|
+
| `mcp.tool.error_message` | Error message if failed (OTLP exporter only) |
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT
|