opentel-mcp 0.2.0 → 0.4.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/CHANGELOG.md +60 -0
- package/README.md +313 -81
- package/package.json +6 -1
- package/src/attributes.js +22 -0
- package/src/config.js +13 -0
- package/src/fingerprint/attributes.js +56 -0
- package/src/fingerprint/classify/auth.js +29 -0
- package/src/fingerprint/classify/dependency.js +32 -0
- package/src/fingerprint/classify/index.js +59 -0
- package/src/fingerprint/classify/internal.js +17 -0
- package/src/fingerprint/classify/network.js +35 -0
- package/src/fingerprint/classify/serialization.js +27 -0
- package/src/fingerprint/classify/timeout.js +29 -0
- package/src/fingerprint/classify/validation.js +29 -0
- package/src/fingerprint/compose.js +149 -0
- package/src/fingerprint/hash.js +39 -0
- package/src/fingerprint/normalize/message.js +30 -0
- package/src/fingerprint/normalize/patterns.js +108 -0
- package/src/fingerprint/normalize/stack.js +165 -0
- package/src/fingerprint/types.d.ts +93 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +4 -0
- package/src/instrument.js +73 -10
- package/src/metrics.js +108 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- OTel metrics, via `@opentelemetry/api`'s Metrics API only (no bundled
|
|
8
|
+
SDK/exporter — same host-app-provides-the-SDK pattern tracing already
|
|
9
|
+
uses):
|
|
10
|
+
- `mcp.tool.calls` (counter) — every tool call; `gen_ai.tool.name`,
|
|
11
|
+
`mcp.method.name`
|
|
12
|
+
- `mcp.tool.errors` (counter) — thrown/rejected handler; `gen_ai.tool.name`,
|
|
13
|
+
`error.type`
|
|
14
|
+
- `mcp.tool.silent_failures` (counter) — JSON-RPC succeeded but
|
|
15
|
+
`CallToolResult.isError === true`; `gen_ai.tool.name`
|
|
16
|
+
- `mcp.tool.duration` (histogram, unit `ms`) — call latency;
|
|
17
|
+
`gen_ai.tool.name`, `mcp.tool.outcome` (`success` | `error` |
|
|
18
|
+
`silent_failure`)
|
|
19
|
+
- `mcp.tool.silent_failures` fires from the same `isError` check that
|
|
20
|
+
marks the span ERROR — extracted into one shared `isToolResultError()`
|
|
21
|
+
helper in `src/instrument.js` so the detection logic isn't duplicated
|
|
22
|
+
between traces and metrics.
|
|
23
|
+
- Metrics are a zero-overhead no-op until the host application registers
|
|
24
|
+
a `MeterProvider` (default `@opentelemetry/api` behavior — not
|
|
25
|
+
special-cased here).
|
|
26
|
+
- `enableMetrics` option (default `true`) to opt out of metric emission
|
|
27
|
+
without affecting tracing.
|
|
28
|
+
|
|
29
|
+
### Naming note (no attribute rename)
|
|
30
|
+
|
|
31
|
+
The tool-name attribute on all four new metrics is `gen_ai.tool.name`, not
|
|
32
|
+
`mcp.tool.name` — the same spec-aligned name spans have used since v0.2's
|
|
33
|
+
semantic-conventions pass (ADR 004). Traces and metrics were already
|
|
34
|
+
consistent going into this release, so nothing was renamed here; this is
|
|
35
|
+
called out because a naive read of the MCP semantic conventions might
|
|
36
|
+
suggest a `mcp.tool.name` attribute, but the spec's actual server-span/
|
|
37
|
+
metric attribute for this is `gen_ai.tool.name` (MCP tool calls are
|
|
38
|
+
GenAI `execute_tool` calls under the hood — see `docs/adr/004-semantic-conventions-alignment.md`
|
|
39
|
+
and `.spec-reference/mcp-semconv.md`). `mcp.method.name`, `error.type`,
|
|
40
|
+
and `mcp.tool.argument_count` are unchanged. `mcp.tool.outcome` is a new
|
|
41
|
+
custom (non-spec) attribute, documented in `src/attributes.js` alongside
|
|
42
|
+
the other custom attribute.
|
|
43
|
+
|
|
44
|
+
### Docs
|
|
45
|
+
|
|
46
|
+
- README: new "Metrics" section (instrument table, `enableMetrics`, and a
|
|
47
|
+
`PeriodicExportingMetricReader` + OTLP/HTTP wiring example targeting
|
|
48
|
+
SigNoz's default local endpoint).
|
|
49
|
+
- Roadmap updated to reflect metrics shipping in this release.
|
|
50
|
+
|
|
51
|
+
## 0.2.0
|
|
52
|
+
|
|
53
|
+
See git history — TypeScript declarations (`.d.ts`), workspace stripping
|
|
54
|
+
for publish, and README documentation improvements.
|
|
55
|
+
|
|
56
|
+
## 0.1.0
|
|
57
|
+
|
|
58
|
+
Initial release: OTel tracing for MCP tool calls, including detection of
|
|
59
|
+
`CallToolResult.isError: true` "silent failures" as `error.type: tool_error`
|
|
60
|
+
span status.
|
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# opentel-mcp
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
> long they take, and which ones fail — via standard OTel traces.
|
|
3
|
+
> Turn every MCP tool call into an OpenTelemetry trace — including the
|
|
4
|
+
> failures your logs won't show you.
|
|
6
5
|
|
|
7
6
|
[](https://github.com/Thirumalaiboobathi/opentel-mcp/actions/workflows/ci.yml)
|
|
8
7
|
[](https://www.npmjs.com/package/opentel-mcp)
|
|
8
|
+
[](https://www.npmjs.com/package/opentel-mcp)
|
|
9
9
|
[](https://github.com/Thirumalaiboobathi/opentel-mcp/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
opentel-mcp watches every tool call your MCP (Model Context Protocol)
|
|
12
|
+
server handles: which tool ran, how long it took, and whether it worked.
|
|
13
|
+
It reports that as OpenTelemetry (OTel) traces — the standard most
|
|
14
|
+
dashboards already read. One function call; no changes to your tools'
|
|
15
|
+
code.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
## The problem
|
|
18
|
+
|
|
19
|
+
Your AI agent calls 15 MCP tools across 3 servers this turn. One tool
|
|
20
|
+
returns `{ isError: true }` inside an otherwise-successful response — how
|
|
21
|
+
a tool reports "I couldn't do that" without crashing. Your logs show
|
|
22
|
+
success. Your metrics show success. The agent gives a wrong answer, and
|
|
23
|
+
nothing you're monitoring says why.
|
|
24
|
+
|
|
25
|
+
opentel-mcp makes that failure visible: one span per tool call, marked as
|
|
26
|
+
an error when it actually is one, using the same standard your dashboards
|
|
27
|
+
already speak.
|
|
16
28
|
|
|
17
29
|
## Install
|
|
18
30
|
|
|
@@ -20,82 +32,121 @@ attribute changes will land in minor versions until `1.0`.
|
|
|
20
32
|
npm install opentel-mcp @opentelemetry/api
|
|
21
33
|
```
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
its package.json, or you must use `.mjs` file extensions.
|
|
25
|
-
|
|
26
|
-
## Why
|
|
35
|
+
opentel-mcp is an ES module — add `"type": "module"` to package.json.
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
visibility into which was slow, which errored silently, which sequence
|
|
30
|
-
ran. opentel-mcp wraps any MCP server and emits one OTel span per tool
|
|
31
|
-
invocation with rich attributes, using standard OpenTelemetry APIs so
|
|
32
|
-
it plugs into your existing observability stack (Jaeger, Grafana Tempo,
|
|
33
|
-
Honeycomb, Datadog, whatever).
|
|
34
|
-
|
|
35
|
-
## Quickstart (5-line usage)
|
|
36
|
-
|
|
37
|
-
Works with either the low-level `Server` API:
|
|
37
|
+
## 30-second quickstart
|
|
38
38
|
|
|
39
39
|
```js
|
|
40
|
-
import {
|
|
40
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
41
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
41
42
|
import { instrumentMcpServer } from 'opentel-mcp';
|
|
43
|
+
import { z } from 'zod';
|
|
42
44
|
|
|
43
|
-
const server = new
|
|
44
|
-
capabilities: { tools: {} }
|
|
45
|
-
});
|
|
45
|
+
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
46
46
|
|
|
47
|
+
// Wraps every tool registered below. Must run BEFORE server.tool() —
|
|
48
|
+
// see "Ordering constraint" below for why.
|
|
47
49
|
instrumentMcpServer(server, {
|
|
48
|
-
serviceName: 'my-mcp-server', //
|
|
49
|
-
setupNodeSdk: true,
|
|
50
|
-
// if you already have OTel configured
|
|
50
|
+
serviceName: 'my-mcp-server', // shows up on your traces
|
|
51
|
+
setupNodeSdk: true, // dev mode: prints traces to your terminal
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
server.
|
|
54
|
+
// A normal tool, registered exactly as usual.
|
|
55
|
+
server.tool('echo', { text: z.string() }, async ({ text }) => ({
|
|
56
|
+
content: [{ type: 'text', text: `you said: ${text}` }],
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const transport = new StdioServerTransport();
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That's it. Every tool call now emits a trace. Wire an exporter to see them
|
|
64
|
+
(next section).
|
|
65
|
+
|
|
66
|
+
## See it working
|
|
67
|
+
|
|
68
|
+
Run the snippet above and this prints to your terminal — a real, captured
|
|
69
|
+
run (full dump: `examples/hello-mcpserver/README.md`):
|
|
70
|
+
|
|
55
71
|
```
|
|
72
|
+
name: 'tools/call echo'
|
|
73
|
+
kind: 1 // SpanKind.SERVER
|
|
74
|
+
status: { code: 1 } // OK
|
|
75
|
+
attributes: {
|
|
76
|
+
'mcp.method.name': 'tools/call',
|
|
77
|
+
'gen_ai.tool.name': 'echo',
|
|
78
|
+
'mcp.tool.argument_count': 1,
|
|
79
|
+
'jsonrpc.request.id': '1'
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
No dashboard needed — `setupNodeSdk: true`'s dev exporter printed this
|
|
84
|
+
directly. Point it at a real backend later; see "Two modes" below.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
The rest of this README goes deeper: both server APIs, every attribute
|
|
89
|
+
and metric emitted, how failure grouping works, configuration, and the
|
|
90
|
+
non-obvious design decisions behind each.
|
|
56
91
|
|
|
57
|
-
|
|
92
|
+
## Both server APIs
|
|
93
|
+
|
|
94
|
+
MCP servers are built on one of two classes from `@modelcontextprotocol/sdk`;
|
|
95
|
+
opentel-mcp detects and wraps either one the same way (see ADR 001 in
|
|
96
|
+
`docs/adr/` for how).
|
|
97
|
+
|
|
98
|
+
**`McpServer`** — the high-level API most servers are actually built on.
|
|
99
|
+
Use it unless you have a specific reason not to; this is what the
|
|
100
|
+
quickstart above uses.
|
|
101
|
+
|
|
102
|
+
**`Server`** — the low-level API, for when you're handling raw JSON-RPC
|
|
103
|
+
yourself or building a library on top of MCP:
|
|
58
104
|
|
|
59
105
|
```js
|
|
60
|
-
import {
|
|
106
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
107
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
108
|
+
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
61
109
|
import { instrumentMcpServer } from 'opentel-mcp';
|
|
62
110
|
|
|
63
|
-
const server = new
|
|
111
|
+
const server = new Server({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
64
112
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
// Must run before setRequestHandler(CallToolRequestSchema, ...) below.
|
|
114
|
+
instrumentMcpServer(server, { serviceName: 'my-mcp-server', setupNodeSdk: true });
|
|
115
|
+
|
|
116
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
117
|
+
const { text } = request.params.arguments ?? {};
|
|
118
|
+
return { content: [{ type: 'text', text: `you said: ${text}` }] };
|
|
68
119
|
});
|
|
69
120
|
|
|
70
|
-
|
|
71
|
-
server.
|
|
121
|
+
const transport = new StdioServerTransport();
|
|
122
|
+
await server.connect(transport);
|
|
72
123
|
```
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
Runnable versions of both live in `examples/hello-server/` and
|
|
126
|
+
`examples/hello-mcpserver/`.
|
|
76
127
|
|
|
77
|
-
##
|
|
128
|
+
## What gets emitted
|
|
129
|
+
|
|
130
|
+
### Tool-level failures, specifically
|
|
78
131
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
names and requirement levels may still change upstream, and this package
|
|
84
|
-
will follow suit when they do. See ADR 004 in `docs/adr/` for the full
|
|
85
|
-
reasoning.
|
|
132
|
+
An MCP tool can fail two ways: it can throw, or it can return
|
|
133
|
+
`isError: true` on an otherwise-successful response (the case from "The
|
|
134
|
+
problem" above). opentel-mcp treats both the same way — span marked
|
|
135
|
+
`ERROR`, nothing thrown, the result returned to the caller unchanged:
|
|
86
136
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
changed) without capturing any argument values.
|
|
137
|
+
```
|
|
138
|
+
tools/call fetch_weather ................. 605ms ERROR
|
|
139
|
+
error.type = tool_error
|
|
140
|
+
```
|
|
92
141
|
|
|
93
|
-
|
|
142
|
+
Verified in `test/instrument.test.js`'s "tool-level failure" tests.
|
|
94
143
|
|
|
95
|
-
Span
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
144
|
+
### Span attributes
|
|
145
|
+
|
|
146
|
+
Every span follows the OpenTelemetry MCP semantic conventions (see
|
|
147
|
+
"Semantic conventions" below), name `{mcp.method.name} {tool name}`
|
|
148
|
+
(e.g. `tools/call echo`), kind `SERVER`, status `ERROR` whenever
|
|
149
|
+
`error.type` is set.
|
|
99
150
|
|
|
100
151
|
| Attribute | Requirement Level | Description | Example |
|
|
101
152
|
|---|---|---|---|
|
|
@@ -107,31 +158,203 @@ back to just `mcp.method.name` when no tool name is available), kind
|
|
|
107
158
|
| mcp.tool.argument_count | **Custom — not spec** | Number of arguments (values not captured) | 2 |
|
|
108
159
|
|
|
109
160
|
Span status description carries the error message on failure (thrown
|
|
110
|
-
errors); no separate error-message attribute
|
|
111
|
-
|
|
161
|
+
errors); there's no separate error-message attribute — the spec expresses
|
|
162
|
+
success/failure through span status, not an attribute. Source of truth:
|
|
163
|
+
`src/attributes.js`.
|
|
112
164
|
|
|
113
|
-
|
|
165
|
+
### Metrics
|
|
166
|
+
|
|
167
|
+
Four `mcp.tool.*` metrics via `@opentelemetry/api`'s Metrics API — same
|
|
168
|
+
API-only pattern as tracing (see "Two modes" below): nothing is recorded
|
|
169
|
+
until a `MeterProvider` is registered. Set `enableMetrics: false` to opt
|
|
170
|
+
out even when one is; tracing is unaffected either way. Source of truth:
|
|
171
|
+
`src/metrics.js`.
|
|
172
|
+
|
|
173
|
+
| Metric | Type | Unit | Attributes | Emitted when |
|
|
174
|
+
|---|---|---|---|---|
|
|
175
|
+
| `mcp.tool.calls` | Counter | — | `gen_ai.tool.name`, `mcp.method.name` | Every tool call |
|
|
176
|
+
| `mcp.tool.errors` | Counter | — | `gen_ai.tool.name`, `error.type`[^1] | Handler threw or rejected |
|
|
177
|
+
| `mcp.tool.silent_failures` | Counter | — | `gen_ai.tool.name`[^1] | Result had `isError: true` |
|
|
178
|
+
| `mcp.tool.duration` | Histogram | ms | `gen_ai.tool.name`, `mcp.tool.outcome`[^1] | Every call, completion |
|
|
179
|
+
|
|
180
|
+
[^1]: Also carries `mcp.failure.category` when fingerprinting finds one — see "Failure Fingerprinting" below.
|
|
181
|
+
|
|
182
|
+
`mcp.tool.silent_failures` increments from the exact same check that marks
|
|
183
|
+
the span `ERROR` (`isToolResultError()` in `src/instrument.js`) — the
|
|
184
|
+
detection logic isn't duplicated between traces and metrics.
|
|
185
|
+
|
|
186
|
+
Wiring a real `MeterProvider`/`TracerProvider` — a worked example against
|
|
187
|
+
SigNoz's local OTLP endpoint:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { metrics, trace } from '@opentelemetry/api';
|
|
191
|
+
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
192
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
193
|
+
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
|
|
194
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
195
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
196
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
197
|
+
import { instrumentMcpServer } from 'opentel-mcp';
|
|
198
|
+
|
|
199
|
+
const resource = resourceFromAttributes({ 'service.name': 'my-mcp-server' });
|
|
114
200
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
201
|
+
const meterProvider = new MeterProvider({
|
|
202
|
+
resource,
|
|
203
|
+
readers: [
|
|
204
|
+
new PeriodicExportingMetricReader({
|
|
205
|
+
exporter: new OTLPMetricExporter({ url: 'http://localhost:4318/v1/metrics' }),
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
210
|
+
|
|
211
|
+
const tracerProvider = new NodeTracerProvider({
|
|
212
|
+
resource,
|
|
213
|
+
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }))],
|
|
214
|
+
});
|
|
215
|
+
tracerProvider.register();
|
|
120
216
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
217
|
+
const server = new Server({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
218
|
+
|
|
219
|
+
// setupNodeSdk: false (default) — both providers above are already
|
|
220
|
+
// registered globally, so instrumentMcpServer() picks them up as-is.
|
|
221
|
+
instrumentMcpServer(server, {});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`http://localhost:4318` is SigNoz's default local OTLP/HTTP (OpenTelemetry
|
|
225
|
+
Protocol — the wire format traces/metrics travel over) endpoint; point it
|
|
226
|
+
at your own collector in production. `@opentelemetry/sdk-metrics` and
|
|
227
|
+
`@opentelemetry/exporter-metrics-otlp-http` are host-app dependencies —
|
|
228
|
+
opentel-mcp doesn't bundle them (see `package.json`'s `peerDependencies`).
|
|
229
|
+
`@opentelemetry/sdk-trace-node` and `@opentelemetry/exporter-trace-otlp-http`
|
|
230
|
+
are already runtime dependencies of opentel-mcp itself (its `setupNodeSdk:
|
|
231
|
+
true` dev path uses them), so no extra install is needed for those two.
|
|
232
|
+
|
|
233
|
+
## Failure Fingerprinting (v0.4.0+)
|
|
234
|
+
|
|
235
|
+
Groups logically identical failures under one stable identifier, even
|
|
236
|
+
when the error message contains UUIDs, timestamps, or user IDs. Ten
|
|
237
|
+
calls that fail the same way but each mention a different user ID show
|
|
238
|
+
up as **one** issue, not ten.
|
|
239
|
+
|
|
240
|
+
Runs locally and synchronously over the error object already in hand —
|
|
241
|
+
no network call, no third-party service. Every thrown error and every
|
|
242
|
+
`isError: true` result gets one, automatically; disable with
|
|
243
|
+
`{ fingerprinting: false }` (default: enabled). Full algorithm: ADR 006
|
|
244
|
+
in `docs/adr/`.
|
|
245
|
+
|
|
246
|
+
| Attribute | Description | Example |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| mcp.failure.fingerprint | Stable 16-hex-char identity for the failure | "a3f4c8e2b1d09f77" |
|
|
249
|
+
| mcp.failure.signature | Human-readable `errorClass@fn:line`, ≤60 chars | "TypeError@doThing:42" |
|
|
250
|
+
| mcp.failure.category | One of 8 categories (below) | "timeout" |
|
|
251
|
+
| mcp.failure.origin | `tool_error` \| `thrown` \| `transport` | "thrown" |
|
|
252
|
+
| mcp.failure.error_class | Error class / constructor name | "TypeError" |
|
|
253
|
+
|
|
254
|
+
Source of truth: `src/fingerprint/attributes.js`. Every category:
|
|
255
|
+
|
|
256
|
+
- `validation` — bad input (Zod/Joi/Yup errors, "invalid"/"required" wording)
|
|
257
|
+
- `timeout` — an operation timed out (`TimeoutError`, `ETIMEDOUT`, ...)
|
|
258
|
+
- `network` — a connection failed (`ECONNREFUSED`, `FetchError`, ...)
|
|
259
|
+
- `auth` — 401/403, "unauthorized"/"forbidden" wording
|
|
260
|
+
- `dependency` — a downstream service or package failed (Mongo, Postgres, ...)
|
|
261
|
+
- `serialization` — malformed JSON, "unexpected token" wording
|
|
262
|
+
- `internal` — nothing more specific matched (the catch-all)
|
|
263
|
+
- `unknown` — fingerprinting itself hit an internal error (should not normally happen)
|
|
264
|
+
|
|
265
|
+
Full classifier source: `src/fingerprint/classify/`.
|
|
266
|
+
|
|
267
|
+
**Cardinality:** the fingerprint itself is unbounded — a new bug means a
|
|
268
|
+
new fingerprint, forever. That's fine on span attributes (each span is
|
|
269
|
+
its own record), but it must **never** go on a metric label, or every
|
|
270
|
+
distinct failure becomes its own permanent time series. opentel-mcp
|
|
271
|
+
enforces this structurally, not by convention: `src/metrics.js` can only
|
|
272
|
+
reach a fingerprint-derived value through
|
|
273
|
+
`METRIC_SAFE_ATTRIBUTES` — a frozen list containing only `category` and
|
|
274
|
+
`origin` (24 combinations max). There is no code path today that could
|
|
275
|
+
accidentally attach `fingerprint`, `signature`, or `error_class` to a
|
|
276
|
+
counter or histogram label. See `src/fingerprint/attributes.js` and ADR
|
|
277
|
+
006's "Consequences" section.
|
|
278
|
+
|
|
279
|
+
**Extending it:** `computeFingerprint(err, ctx, opts)`
|
|
280
|
+
(`src/fingerprint/compose.js`) accepts `opts.classifiers` to prepend your
|
|
281
|
+
own detection rules ahead of the built-in eight, and `opts.stackFrames` to
|
|
282
|
+
change how many stack frames feed the signature — see
|
|
283
|
+
`test/fingerprint/compose.test.js`'s "uses a custom classifiers list" and
|
|
284
|
+
"respects a custom opts.stackFrames count" tests, and
|
|
285
|
+
`examples/fingerprint-demo.js` for a runnable, standalone demo (`node
|
|
286
|
+
examples/fingerprint-demo.js`). Not yet wired through
|
|
287
|
+
`instrumentMcpServer()`'s own options — today this means importing
|
|
288
|
+
`computeFingerprint` directly rather than configuring the automatic
|
|
289
|
+
per-call-site wrapping; tracked in the roadmap below.
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
All options passed to `instrumentMcpServer(server, options)`. Source of
|
|
294
|
+
truth: `src/config.js`.
|
|
295
|
+
|
|
296
|
+
| Option | Type | Default | Description |
|
|
297
|
+
|---|---|---|---|
|
|
298
|
+
| `serviceName` | string | — | Resource name for traces[^2] |
|
|
299
|
+
| `setupNodeSdk` | boolean | `false` | Dev mode: stderr tracer, no setup[^3] |
|
|
300
|
+
| `exporterUrl` | string | — | OTLP/HTTP traces endpoint[^4] |
|
|
301
|
+
| `enabled` | boolean | `true` | `false` disables all instrumentation |
|
|
302
|
+
| `enableMetrics` | boolean | `true` | `false` disables `mcp.tool.*` metrics only |
|
|
303
|
+
| `fingerprinting` | boolean | `true` | `false` disables `mcp.failure.*` attributes |
|
|
304
|
+
|
|
305
|
+
[^2]: Required only when `setupNodeSdk` is `true`. Has no effect otherwise — the host app's registered `TracerProvider` owns the resource; passing it anyway logs a one-time `diag.warn`.
|
|
306
|
+
[^3]: Creates and registers a `NodeTracerProvider` that always prints to stderr (safe alongside stdio-transport servers — ADR 003), additionally exporting via OTLP/HTTP if `exporterUrl` is set.
|
|
307
|
+
[^4]: Only takes effect when `setupNodeSdk` is `true`.
|
|
128
308
|
|
|
129
309
|
## Ordering constraint
|
|
130
310
|
|
|
131
|
-
|
|
311
|
+
Instrumentation works by wrapping the tool-call handler at the moment
|
|
312
|
+
it's registered. If a handler is registered before `instrumentMcpServer()`
|
|
313
|
+
runs, that handler was never wrapped — it slipped past the trap before it
|
|
314
|
+
was set.
|
|
315
|
+
|
|
316
|
+
Call `instrumentMcpServer()` **before** registering any tool handlers —
|
|
132
317
|
before `server.setRequestHandler(CallToolRequestSchema, ...)` (low-level
|
|
133
318
|
`Server`) or before any `.tool()`/`.registerTool()` call (`McpServer`).
|
|
134
|
-
See ADR 002 in docs/adr
|
|
319
|
+
See ADR 002 in `docs/adr/` for the detection logic that catches violations
|
|
320
|
+
of this at instrument time.
|
|
321
|
+
|
|
322
|
+
## Two modes
|
|
323
|
+
|
|
324
|
+
### Quick dev setup
|
|
325
|
+
|
|
326
|
+
`setupNodeSdk: true` sets up a `NodeTracerProvider` that prints spans to
|
|
327
|
+
stderr (safe alongside stdio-transport MCP servers — see ADR 003),
|
|
328
|
+
optionally plus an OTLP exporter if `exporterUrl` is provided. No separate
|
|
329
|
+
OTel SDK setup needed — `serviceName` is required in this mode, since it
|
|
330
|
+
names the resource of the provider opentel-mcp creates.
|
|
331
|
+
|
|
332
|
+
### Production setup
|
|
333
|
+
|
|
334
|
+
Omit `setupNodeSdk` (default `false`). opentel-mcp uses whatever
|
|
335
|
+
`TracerProvider` is already registered via
|
|
336
|
+
`trace.setGlobalTracerProvider()`, so it plugs into any existing OTel
|
|
337
|
+
setup without conflict. The host's `TracerProvider` owns the resource
|
|
338
|
+
here, so `serviceName` is not needed and has no effect — set
|
|
339
|
+
`service.name` on the host's `Resource` instead. Passing `serviceName`
|
|
340
|
+
anyway is harmless but logs a one-time `diag.warn`.
|
|
341
|
+
|
|
342
|
+
## Semantic conventions
|
|
343
|
+
|
|
344
|
+
`0.x` — the [MCP semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai)
|
|
345
|
+
this library implements are Development-stage, not Stable, and may still
|
|
346
|
+
change upstream; breaking attribute renames will land in minor versions
|
|
347
|
+
until `1.0`, tracked in release notes rather than silently shipped.
|
|
348
|
+
|
|
349
|
+
opentel-mcp follows those conventions (published by the OTel GenAI SIG,
|
|
350
|
+
moved there from the main `semantic-conventions` repo, where the MCP
|
|
351
|
+
conventions are now deprecated) for everything they define, and adds two
|
|
352
|
+
namespaces of its own where they don't yet: `mcp.tool.*` (call-count and
|
|
353
|
+
duration metrics) and `mcp.failure.*` (failure fingerprinting). Both are
|
|
354
|
+
documented as non-spec at every attribute (`src/attributes.js`,
|
|
355
|
+
`src/fingerprint/attributes.js`), and are candidates to fold into the
|
|
356
|
+
spec's own metrics/error vocabulary if it grows an equivalent. Full
|
|
357
|
+
reasoning: ADR 004 in `docs/adr/`.
|
|
135
358
|
|
|
136
359
|
## Compatibility
|
|
137
360
|
|
|
@@ -141,15 +364,24 @@ See ADR 002 in docs/adr/ for why.
|
|
|
141
364
|
- Supports both low-level `Server` and high-level `McpServer` APIs
|
|
142
365
|
- @modelcontextprotocol/sdk ^1.0.0
|
|
143
366
|
- @opentelemetry/api ^1.9.0
|
|
367
|
+
- 127 tests (`npm test`) — see `test/`
|
|
144
368
|
|
|
145
369
|
## Roadmap
|
|
146
370
|
|
|
147
|
-
- v0.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
371
|
+
- v0.4: Deep Failure Fingerprinting ✓ — see "Failure Fingerprinting" above
|
|
372
|
+
and ADR 006.
|
|
373
|
+
- v0.5: Failure clustering + regression detection
|
|
374
|
+
- Future: recovery hints, root-cause chaining across parent spans,
|
|
375
|
+
alignment with the OTel GenAI SIG's MCP semantic conventions when
|
|
376
|
+
published
|
|
377
|
+
- Also still tracked, not silently dropped: exposing `computeFingerprint`'s
|
|
378
|
+
`classifiers`/`stackFrames` options through `instrumentMcpServer()`
|
|
379
|
+
itself; opt-in `gen_ai.tool.call.arguments` support with a redaction
|
|
380
|
+
callback; the spec's own `mcp.server.operation.duration` /
|
|
381
|
+
`mcp.server.session.duration` metrics; W3C trace context propagation via
|
|
382
|
+
`params._meta` per
|
|
383
|
+
[SEP-414](https://modelcontextprotocol.io/community/seps/414-request-meta);
|
|
384
|
+
and client-side instrumentation, so a single trace can span the client
|
|
153
385
|
call and the server's tool execution
|
|
154
386
|
|
|
155
387
|
## Contributing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentel-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "One-line OpenTelemetry instrumentation for Model Context Protocol (MCP) servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
16
|
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"engines": {
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
"scripts": {
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"test:watch": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"bench": "vitest bench --run",
|
|
25
28
|
"prepack": "node scripts/strip-workspaces.js",
|
|
26
29
|
"postpack": "node scripts/restore-workspaces.js"
|
|
27
30
|
},
|
|
@@ -56,7 +59,9 @@
|
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@opentelemetry/api": "^1.9.0",
|
|
62
|
+
"@opentelemetry/sdk-metrics": "^2.9.0",
|
|
59
63
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
64
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
60
65
|
"typescript": "^7.0.2",
|
|
61
66
|
"vitest": "^2.1.8"
|
|
62
67
|
}
|
package/src/attributes.js
CHANGED
|
@@ -63,3 +63,25 @@ export const ATTR_MCP_TOOL_ARGUMENT_COUNT = 'mcp.tool.argument_count';
|
|
|
63
63
|
|
|
64
64
|
export const ATTR_MCP_SERVER_NAME = 'mcp.server.name';
|
|
65
65
|
export const ATTR_MCP_SERVER_VERSION = 'mcp.server.version';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* NOT part of the MCP semantic conventions. Our own addition, on the
|
|
69
|
+
* mcp.tool.duration histogram (see src/metrics.js): which of the three
|
|
70
|
+
* call outcomes a given duration measurement belongs to. The spec's
|
|
71
|
+
* mcp.server.operation.duration metric (not yet implemented here — see
|
|
72
|
+
* README roadmap) expresses failure only via error.type; this attribute
|
|
73
|
+
* additionally distinguishes "thrown/protocol error" from "silent failure"
|
|
74
|
+
* (isError: true) so both are visible on the same histogram without
|
|
75
|
+
* requiring a join against error.type, which silent failures don't set
|
|
76
|
+
* on the duration metric.
|
|
77
|
+
*/
|
|
78
|
+
export const ATTR_MCP_TOOL_OUTCOME = 'mcp.tool.outcome';
|
|
79
|
+
|
|
80
|
+
/** Well-known mcp.tool.outcome value: the call succeeded. */
|
|
81
|
+
export const MCP_TOOL_OUTCOME_SUCCESS = 'success';
|
|
82
|
+
|
|
83
|
+
/** Well-known mcp.tool.outcome value: the handler threw or its promise rejected. */
|
|
84
|
+
export const MCP_TOOL_OUTCOME_ERROR = 'error';
|
|
85
|
+
|
|
86
|
+
/** Well-known mcp.tool.outcome value: isError: true (see ERROR_TYPE_TOOL_ERROR above). */
|
|
87
|
+
export const MCP_TOOL_OUTCOME_SILENT_FAILURE = 'silent_failure';
|
package/src/config.js
CHANGED
|
@@ -16,12 +16,23 @@ import { diag } from '@opentelemetry/api';
|
|
|
16
16
|
* Only takes effect when `setupNodeSdk` is true.
|
|
17
17
|
* @property {boolean} [enabled=true] - Set to false to disable instrumentation entirely; instrumentMcpServer()
|
|
18
18
|
* becomes a no-op.
|
|
19
|
+
* @property {boolean} [enableMetrics=true] - Set to false to disable the mcp.tool.* metrics (tracing is
|
|
20
|
+
* unaffected). Metrics are already a zero-overhead no-op when no MeterProvider is registered — the default
|
|
21
|
+
* @opentelemetry/api behavior — so this flag exists for opting out even when one *is* registered, not as a
|
|
22
|
+
* substitute for that default.
|
|
19
23
|
* @property {boolean} [setupNodeSdk=false] - When true, instrumentMcpServer() creates and registers its own
|
|
20
24
|
* NodeTracerProvider (always exporting to stderr — safe alongside stdio-transport MCP servers, see ADR 003;
|
|
21
25
|
* additionally to `exporterUrl` via OTLP/HTTP if set). When false (the default), spans are emitted via
|
|
22
26
|
* whatever OpenTelemetry TracerProvider the host application
|
|
23
27
|
* has already registered globally — or dropped silently if none has been registered. This default keeps
|
|
24
28
|
* instrumentMcpServer() from ever overriding a host application's own OpenTelemetry setup.
|
|
29
|
+
* @property {boolean} [fingerprinting=true] - Set to false to disable deep-failure fingerprinting. When enabled
|
|
30
|
+
* (the default), every thrown error and tool-level failure (isError: true) is run through
|
|
31
|
+
* src/fingerprint/compose.js's computeFingerprint(), adding mcp.failure.* span attributes and an
|
|
32
|
+
* mcp.failure.category attribute on the mcp.tool.errors / mcp.tool.silent_failures / mcp.tool.duration
|
|
33
|
+
* metrics (see src/fingerprint/attributes.js). computeFingerprint() never throws, so this only trades a
|
|
34
|
+
* small amount of per-failure CPU (see the p99 < 200µs budget in test/fingerprint/benchmark.test.js) for
|
|
35
|
+
* fingerprinting.
|
|
25
36
|
*/
|
|
26
37
|
|
|
27
38
|
// Guards the "serviceName has no effect" diagnostic below so it fires once
|
|
@@ -66,6 +77,8 @@ export function resolveOptions(options) {
|
|
|
66
77
|
serviceName: opts.serviceName,
|
|
67
78
|
exporterUrl: opts.exporterUrl,
|
|
68
79
|
enabled: opts.enabled ?? true,
|
|
80
|
+
enableMetrics: opts.enableMetrics ?? true,
|
|
69
81
|
setupNodeSdk,
|
|
82
|
+
fingerprinting: opts.fingerprinting ?? true,
|
|
70
83
|
};
|
|
71
84
|
}
|