opentel-mcp 0.14.0 → 0.16.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 CHANGED
@@ -1,5 +1,192 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0
4
+
5
+ Finishes what 0.15 started. Telemetry now survives the way MCP clients
6
+ actually stop servers (`flushOnExit`), and two new signals land: errors
7
+ your agent can't act on, and opt-in resources/prompts coverage. Plus a fix
8
+ for SDK v2 servers that declare `capabilities.tools`.
9
+ `setupNodeSdk: false` (production) behavior is unchanged apart from the
10
+ new span attributes on `isError: true` results and the v2 fix; resources/
11
+ prompts coverage is off unless you opt in. No new metric label keys.
12
+
13
+ ### Added
14
+
15
+ - **`flushOnExit` (ADR 024, `docs/adr/024-flush-on-exit.md`).** Under
16
+ `setupNodeSdk: true`, buffered spans and dev-mode metrics are now
17
+ flushed automatically when the process stops: on `beforeExit` (stdin
18
+ closed, loop drained), `SIGTERM` and `SIGINT`. MCP clients built on the
19
+ SDK's stdio transport stop servers with exactly that sequence (stdin
20
+ close → SIGTERM → SIGKILL, 2 s apart); the old `process.on('beforeExit')`
21
+ snippet only covered the first step. On by default with
22
+ `setupNodeSdk: true`; `flushOnExit: false` opts out;
23
+ `flushOnExit: { timeoutMs }` lowers the 1000 ms cap.
24
+ - One listener set per process, however many servers are instrumented,
25
+ and it's removed again once every server has shut down.
26
+ - After flushing, a signal is re-raised when no other listener for it
27
+ exists, so the process still dies by that signal. If your code has its
28
+ own listener, yours decides. `beforeExit` never changes the exit code.
29
+ - **No effect at all with `setupNodeSdk: false`** (production
30
+ behavior unchanged): passing it there logs a one-time `diag.warn`.
31
+ - Not covered: `process.exit()` (call `await server.shutdown()` first)
32
+ and `SIGKILL`.
33
+
34
+ - **Unactionable tool errors (ADR 025, `docs/adr/025-unactionable-errors.md`).**
35
+ Every `isError: true` span now carries `mcp.failure.unactionable`
36
+ (boolean) and `mcp.failure.content_length_bucket` (`empty` | `tiny` |
37
+ `short` | `medium` | `long`). Unactionable means no content, or under
38
+ 10 characters of trimmed text with no image/audio/resource item: an
39
+ error that gives the agent nothing to act on. Computed only from text
40
+ lengths and non-text item counts, never from the text; a canary-string
41
+ test checks every exported span and metric. Span-only (no metric label,
42
+ `METRIC_SAFE_ATTRIBUTES` unchanged); fingerprints and categories
43
+ unchanged. Configure with `unactionableErrors: { enabled, minTextLength }`
44
+ or `OTEL_MCP_UNACTIONABLE_ERRORS_ENABLED` /
45
+ `OTEL_MCP_UNACTIONABLE_ERRORS_MIN_TEXT_LENGTH`.
46
+
47
+ - **Resources and prompts coverage, opt-in (ADR 026,
48
+ `docs/adr/026-resources-prompts-coverage.md`).**
49
+ `coverage: { resources: true, prompts: true }` also traces
50
+ `resources/read`, `resources/list`, `resources/templates/list`,
51
+ `prompts/get` and `prompts/list`, on SDK v1 and v2, low-level `Server`
52
+ and `McpServer`. Both are off by default; nothing changes unless you opt in.
53
+ - Spans are named by method (`prompts/get <name>` for prompts/get) and
54
+ carry `mcp.method.name` (+ `gen_ai.prompt.name` on prompts/get, capped
55
+ at 128 characters). `gen_ai.tool.name` is absent.
56
+ - Never captured: resource URIs, prompt arguments, result content, or
57
+ the exception message of a failing request (SDK resource errors embed
58
+ the URI). Failures get status `ERROR`, `error.type` and the usual
59
+ fingerprint/category/channel.
60
+ - New histogram `mcp.server.operation.duration`, labeled by
61
+ `mcp.method.name` + `error.type` only. No new label keys;
62
+ `mcp.method.name` stays a fixed set of constants (now 7 values).
63
+ - A handler already registered when `instrumentMcpServer()` runs is
64
+ skipped with one `diag.warn`, never a throw. That includes SDK v2
65
+ `McpServer` declaring `capabilities.resources/prompts` in its
66
+ constructor.
67
+
68
+ ### Changed
69
+
70
+ - `server.shutdown()` (under `setupNodeSdk: true`) is now idempotent:
71
+ every call returns the same promise, so an explicit `shutdown()` and
72
+ `flushOnExit` never shut the providers down twice.
73
+ - The README quickstart and both examples no longer need the
74
+ `process.on('beforeExit', ...)` block; it's been removed.
75
+
76
+ ### Fixed
77
+
78
+ - **SDK v2: a `McpServer` constructed with `capabilities: { tools: {} }`
79
+ can now be instrumented.** v2's `McpServer` installs its own
80
+ `tools/list`/`tools/call` dispatchers in its constructor when that
81
+ capability is declared, so `instrumentMcpServer()` threw
82
+ `INSTRUMENT_FIRST_ERROR` on every such server, whatever the call order.
83
+ It now recognizes McpServer's own constructor-installed handlers (v2
84
+ McpServer, installer ran, no tool registered yet, not yet connected),
85
+ takes them down with the public `removeRequestHandler()`, and lets
86
+ McpServer re-install them through the instrumented `setRequestHandler`,
87
+ so they're wrapped like any lazily-installed handler. Genuine misorders
88
+ still throw: a tool registered before instrumenting (v1 or v2, with or
89
+ without declared capabilities) or a low-level `Server` handler
90
+ registered first. Present since v2 support shipped (SDK 2.0.0).
91
+
92
+ ## 0.15.0
93
+
94
+ "Make it visible" — no instrumentation changes, no `tools/call` hot-path
95
+ changes. Phase 1 (ADR 022, `docs/adr/022-publish-ui.md`): publishes
96
+ `opentel-mcp-ui`, previously a working but `"private": true`, unpublished
97
+ scaffold with zero mentions in this README. See
98
+ [`opentel-mcp-ui`'s own CHANGELOG](https://www.npmjs.com/package/opentel-mcp-ui)
99
+ for what changed in that package. Phase 2 (ADR 023,
100
+ `docs/adr/023-dev-mode-metrics.md`): dev-mode metrics under
101
+ `setupNodeSdk: true` — see "Added" below.
102
+
103
+ ### Added
104
+
105
+ - README: a "See it locally" section near the top, pointing at
106
+ `npx opentel-mcp-ui --demo` — this library had no documented way for a
107
+ new reader to see its own output before installing anything.
108
+ - `setupNodeSdk: true` now also stands up an owned `MeterProvider` (ADR
109
+ 023): a `PeriodicExportingMetricReader` exporting all 12 `mcp.tool.*`/
110
+ `mcp.tool.loop.*`/`mcp.tool.schema_drift.*` instruments to stderr every
111
+ 5 seconds, via a new `StderrMetricExporter` (compact
112
+ `name{attrs} = value` lines, never a stdout-writing JSON dump — same
113
+ ADR 003 reasoning `StderrSpanExporter` already follows). Gated
114
+ additionally by `enableMetrics` (default `true`, unchanged option).
115
+ Closes the gap where the documented 30-second quickstart emitted
116
+ stderr spans but none of its 12 metrics. `setupNodeSdk: false` (the
117
+ default, and the production posture) is completely unaffected — every
118
+ new code path is gated behind the same existing `setupNodeSdk === true`
119
+ check `StderrSpanExporter` already uses.
120
+ - If a `MeterProvider` is already registered globally when this runs
121
+ (your own, or anything else's), this is a complete no-op: nothing is
122
+ overridden, no exception is thrown, and a single `diag.warn` (once
123
+ per process, not once per `instrumentMcpServer()` call) names the
124
+ situation. Detected via `@opentelemetry/api`'s own documented
125
+ `boolean` return from `setGlobalMeterProvider()` — not by inspecting
126
+ internals. The abandoned candidate provider built before that check
127
+ is shut down immediately (fire-and-forget), closing a leak where its
128
+ own periodic reader would otherwise tick forever, unused, in the
129
+ background.
130
+ - `server.shutdown()` (already attached under `setupNodeSdk: true`) now
131
+ also flushes and shuts down this `MeterProvider`, alongside the
132
+ existing `TracerProvider` shutdown it already performed.
133
+ - `StderrMetricExporter` only reprints a metric when its value has
134
+ actually changed since the last export. Every instrument here is
135
+ CUMULATIVE, so without this a single early tool call would otherwise
136
+ reprint two unchanged lines every 5 seconds for the rest of the
137
+ process's life — confirmed empirically before this fix shipped, not
138
+ assumed.
139
+
140
+ ### Changed
141
+
142
+ - **`@opentelemetry/sdk-metrics` is now a runtime (`dependencies`, not
143
+ `devDependencies`) dependency of `opentel-mcp`, installed for every
144
+ user — including under `setupNodeSdk: false`, which never executes any
145
+ code from it.** Needed at import time for the feature above; moving it
146
+ to `dependencies` means `npm install opentel-mcp` always pulls it in,
147
+ regardless of whether a given deployment ever sets `setupNodeSdk: true`.
148
+ Adds no new transitive dependency: its own two dependencies
149
+ (`@opentelemetry/core`, `@opentelemetry/resources`) were already in
150
+ this package's tree via `@opentelemetry/sdk-trace-node`/the existing
151
+ `@opentelemetry/resources` dependency. See ADR 023's "Dependency
152
+ strategy" section for the full reasoning, including why an optional-peer
153
+ + dynamic-import approach was considered and rejected.
154
+
155
+ ### Fixed
156
+
157
+ - The example servers (`examples/hello-server`, `examples/hello-mcpserver`)
158
+ never showed a metrics line in their own READMEs' "What you'll see": a
159
+ single piped `tools/call` request exits the process before the 5-second
160
+ export interval. Both now flush once via `process.on('beforeExit', ...)`
161
+ right before the process would otherwise exit — confirmed
162
+ `StdioServerTransport` does not call its own `close()`/`onclose` on
163
+ stdin EOF, so `beforeExit` is the one hook that still allows the async
164
+ flush to complete first.
165
+ - Documentation only, no code/behavior changes:
166
+ - README claimed "Four `mcp.tool.*` metrics" in a way that read as the
167
+ library's total metric count. Corrected: four is this section's own
168
+ base call/duration metrics; twelve exist in total across cost/token
169
+ attribution (2), Agent Thrash Detection (5), and schema drift (1),
170
+ each already documented in their own sections.
171
+ - README's test count was stale at every point it was checked this
172
+ release; now 1017 tests, 1013 passing + 4 intentionally skipped.
173
+ - README's SigNoz metrics-wiring example described
174
+ `@opentelemetry/sdk-metrics` as a host-app dependency opentel-mcp
175
+ "doesn't bundle" — true before this release, no longer true now that
176
+ it's a real dependency (see "Changed" above).
177
+
178
+ ### CI
179
+
180
+ - `dependency-audit` (`.github/workflows/ci.yml`) no longer audits this
181
+ monorepo's own shared, hoisted `node_modules` — `npm audit`, even with
182
+ `--omit=dev` and run from inside `packages/core/`, scans the entire
183
+ installed tree regardless of cwd or `--workspace` (confirmed
184
+ empirically), so it was reporting findings from `examples/*`'s real
185
+ `@modelcontextprotocol/sdk` dependency (express/hono/ajv transitives)
186
+ as if they were this library's own. Now packs and installs each of
187
+ `packages/core`/`packages/ui` into its own clean, throwaway project —
188
+ the exact tree a real `npm install` produces — and audits that instead.
189
+
3
190
  ## 0.14.0
4
191
 
5
192
  Adds `errorRecording.redactor`: a host-supplied hook for content
package/README.md CHANGED
@@ -18,6 +18,26 @@ to your tools' code.
18
18
  opentel-mcp is the only Node.js MCP instrumentation library that ties
19
19
  tool calls to LLM cost.
20
20
 
21
+ ## See it locally
22
+
23
+ ```bash
24
+ npx opentel-mcp-ui --demo
25
+ ```
26
+
27
+ Opens a dashboard at `http://127.0.0.1:4319` — no MCP server, no
28
+ Prometheus, no Collector to stand up first. It's seeded with realistic
29
+ fixture data showing exactly what this README is about: tool calls that
30
+ returned `isError: true` inside a successful response, rendered as the
31
+ clean spans a standard OTel setup would show, side by side with what
32
+ opentel-mcp actually caught.
33
+
34
+ <!-- TODO: screenshot/GIF of the dashboard here -->
35
+
36
+ Drop `--demo` and point your own instrumented server's `exporterUrl` at
37
+ it to see real traffic instead — see
38
+ [`opentel-mcp-ui`'s README](https://www.npmjs.com/package/opentel-mcp-ui)
39
+ for the two-minute walkthrough.
40
+
21
41
  ## The problem
22
42
 
23
43
  Your AI agent calls 15 MCP tools across 3 servers this turn. One tool
@@ -38,6 +58,15 @@ npm install opentel-mcp @opentelemetry/api
38
58
 
39
59
  opentel-mcp is an ES module — add `"type": "module"` to package.json.
40
60
 
61
+ If you're starting from a blank project to try the quickstart below
62
+ (rather than adding opentel-mcp to an MCP server you already have), you
63
+ also need the MCP SDK and `zod` — the quickstart's own server and tool
64
+ definitions, not opentel-mcp's dependencies:
65
+
66
+ ```bash
67
+ npm install @modelcontextprotocol/sdk zod
68
+ ```
69
+
41
70
  ## 30-second quickstart
42
71
 
43
72
  ```js
@@ -65,12 +94,22 @@ await server.connect(transport);
65
94
  ```
66
95
 
67
96
  That's it. Every tool call now emits a trace. Wire an exporter to see them
68
- (next section).
97
+ (next section). When the server stops — your MCP client closing stdin, or
98
+ sending SIGTERM/SIGINT — anything still buffered is flushed first,
99
+ automatically (see "Flushing on exit" below).
69
100
 
70
101
  ## See it working
71
102
 
72
- Run the snippet above and this prints to your terminal — a real, captured
73
- run (full dump: `examples/hello-mcpserver/README.md`):
103
+ Save the snippet as `server.js`. On its own it just waits: an MCP server
104
+ reads requests from stdin and does nothing until a client sends one. To
105
+ try it without a client, pipe in a single tool call:
106
+
107
+ ```bash
108
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}' | node server.js
109
+ ```
110
+
111
+ The JSON-RPC response goes to stdout, and this prints to stderr — a real,
112
+ captured run (full dump: `examples/hello-mcpserver/README.md`):
74
113
 
75
114
  ```
76
115
  name: 'tools/call echo'
@@ -87,6 +126,23 @@ attributes: {
87
126
  No dashboard needed — `setupNodeSdk: true`'s dev exporter printed this
88
127
  directly. Point it at a real backend later; see "Two modes" below.
89
128
 
129
+ **Metrics print too (v0.15.0+)** — a compact one-line-per-metric summary
130
+ on stderr, reusing the exact `mcp.tool.*` instruments documented in
131
+ "Metrics" below. A long-running server prints it every 5 seconds (not the
132
+ default 60-second export interval). The one-shot piped run above prints it
133
+ once as it exits, because the server flushes when stdin closes (v0.16.0,
134
+ see "Flushing on exit"):
135
+
136
+ ```
137
+ [opentel-mcp metrics] mcp.tool.calls{gen_ai.tool.name=echo,mcp.method.name=tools/call} = 1
138
+ [opentel-mcp metrics] mcp.tool.duration{gen_ai.tool.name=echo,mcp.tool.outcome=success} count=1 avg=0.4ms
139
+ ```
140
+
141
+ No extra configuration, no `@opentelemetry/sdk-metrics` setup of your
142
+ own — `setupNodeSdk: true` now does for metrics what it's always done for
143
+ traces. See "Dev-mode metrics" below for exactly what this does and
144
+ doesn't change.
145
+
90
146
  ---
91
147
 
92
148
  The rest of this README goes deeper: both server APIs, every attribute
@@ -240,7 +296,17 @@ Runnable end-to-end coverage lives in `test/instrument.v2.test.js` and
240
296
 
241
297
  An MCP tool can fail two ways: it can throw, or it can return
242
298
  `isError: true` on an otherwise-successful response (the case from "The
243
- problem" above). opentel-mcp treats both the same way — span marked
299
+ problem" above) — exactly like the quickstart's `echo` tool, but with
300
+ `isError: true` added and no `throw` involved:
301
+
302
+ ```js
303
+ server.tool('fetch_weather', { city: z.string() }, async ({ city }) => ({
304
+ isError: true,
305
+ content: [{ type: 'text', text: `couldn't reach the weather service for ${city}` }],
306
+ }));
307
+ ```
308
+
309
+ opentel-mcp treats both failure shapes the same way — span marked
244
310
  `ERROR`, nothing thrown, the result returned to the caller unchanged:
245
311
 
246
312
  ```
@@ -250,6 +316,60 @@ error.type = tool_error
250
316
 
251
317
  Verified in `test/instrument.test.js`'s "tool-level failure" tests.
252
318
 
319
+ #### Errors your agent can't act on (v0.16.0)
320
+
321
+ An `isError: true` result with no content, or just `"Error"`, tells the
322
+ model nothing about what went wrong or what to do next — a common cause
323
+ of retries and thrash. Every `isError: true` span also carries:
324
+
325
+ | Attribute | Values |
326
+ | --- | --- |
327
+ | `mcp.failure.unactionable` | `true` when there's no content, or fewer than 10 characters of text (whitespace-trimmed) and no image/audio/resource item |
328
+ | `mcp.failure.content_length_bucket` | `empty` (0) · `tiny` (1–9) · `short` (< 80) · `medium` (< 500) · `long` |
329
+
330
+ Both are computed **only from lengths and item counts — never from the
331
+ text itself** (proved by a canary-string test in
332
+ `test/unactionable.test.js`), on spans only, never as metric labels. They
333
+ don't change fingerprints or categories. Configure with
334
+ `unactionableErrors: { enabled, minTextLength }` (defaults `true`, `10`).
335
+ Design: [ADR 025](../../docs/adr/025-unactionable-errors.md).
336
+
337
+ ### Resources and prompts (opt-in, v0.16.0)
338
+
339
+ Tools are traced by default. `resources/*` and `prompts/*` requests can be
340
+ traced too, if you turn them on:
341
+
342
+ ```js
343
+ instrumentMcpServer(server, {
344
+ coverage: { resources: true, prompts: true }, // either or both
345
+ });
346
+ ```
347
+
348
+ | Method | Span name | Attributes |
349
+ | --- | --- | --- |
350
+ | `resources/read`, `resources/list`, `resources/templates/list` | the method, e.g. `resources/read` | `mcp.method.name` |
351
+ | `prompts/get` | `prompts/get <prompt name>` | `mcp.method.name`, `gen_ai.prompt.name` |
352
+ | `prompts/list` | `prompts/list` | `mcp.method.name` |
353
+
354
+ - **Never captured:** resource URIs, prompt arguments, result content. A
355
+ failing request gets status `ERROR`, `error.type`, and the usual
356
+ fingerprint/category/channel (`mcp.failure.*`), but **not** the exception
357
+ message, because SDK resource errors embed the URI in it.
358
+ - `gen_ai.tool.name` is absent (not empty) on these spans, so they never
359
+ show up in per-tool views or tool metrics.
360
+ - Durations go to a separate histogram, `mcp.server.operation.duration`,
361
+ labeled only by `mcp.method.name` (one of the five methods above) and
362
+ `error.type`. No thrash detection or cost tracking applies.
363
+ - **Ordering:** instrument before registering resources/prompts, same as
364
+ tools. If a handler for one of these methods already exists, that method
365
+ is skipped with a single `diag.warn`. It never throws, so turning this on
366
+ can't break an existing server. On an SDK v2 `McpServer`, declaring
367
+ `capabilities: { resources: {}, prompts: {} }` in the constructor
368
+ registers those handlers immediately (and so skips them); leave them out,
369
+ since registering a resource or prompt declares the capability for you.
370
+
371
+ Design: [ADR 026](../../docs/adr/026-resources-prompts-coverage.md).
372
+
253
373
  ### Span attributes
254
374
 
255
375
  Every span follows the OpenTelemetry MCP semantic conventions (see
@@ -273,11 +393,17 @@ success/failure through span status, not an attribute. Source of truth:
273
393
 
274
394
  ### Metrics
275
395
 
276
- Four `mcp.tool.*` metrics via `@opentelemetry/api`'s Metrics API — same
277
- API-only pattern as tracing (see "Two modes" below): nothing is recorded
278
- until a `MeterProvider` is registered. Set `enableMetrics: false` to opt
279
- out even when one is; tracing is unaffected either way. Source of truth:
280
- `src/metrics.js`.
396
+ The four base `mcp.tool.*` call/duration metrics below, via
397
+ `@opentelemetry/api`'s Metrics API — same API-only pattern as tracing
398
+ (see "Two modes" below): nothing is recorded until a `MeterProvider` is
399
+ registered — **except under `setupNodeSdk: true`, which registers one for
400
+ you (v0.15.0+); see "Dev-mode metrics" below.** Set `enableMetrics: false`
401
+ to opt out even when one is (dev-mode included); tracing is unaffected
402
+ either way. Source of truth: `src/metrics.js`. Twelve metrics exist
403
+ across the library in total — this section's four, plus two cost/token
404
+ metrics ("Cost & Token Attribution" below), five Agent Thrash Detection
405
+ metrics ("Agent Thrash Detection" below), and one schema-drift metric
406
+ ("Tool schema drift detection" below).
281
407
 
282
408
  | Metric | Type | Unit | Attributes | Emitted when |
283
409
  |---|---|---|---|---|
@@ -332,12 +458,83 @@ instrumentMcpServer(server, {});
332
458
 
333
459
  `http://localhost:4318` is SigNoz's default local OTLP/HTTP (OpenTelemetry
334
460
  Protocol — the wire format traces/metrics travel over) endpoint; point it
335
- at your own collector in production. `@opentelemetry/sdk-metrics` and
336
- `@opentelemetry/exporter-metrics-otlp-http` are host-app dependencies —
337
- opentel-mcp doesn't bundle them (see `package.json`'s `peerDependencies`).
338
- `@opentelemetry/sdk-trace-node` and `@opentelemetry/exporter-trace-otlp-http`
339
- are already runtime dependencies of opentel-mcp itself (its `setupNodeSdk:
340
- true` dev path uses them), so no extra install is needed for those two.
461
+ at your own collector in production. `@opentelemetry/exporter-metrics-otlp-http`
462
+ is a host-app dependency — opentel-mcp doesn't bundle it. `@opentelemetry/sdk-metrics`,
463
+ `@opentelemetry/sdk-trace-node`, and `@opentelemetry/exporter-trace-otlp-http`
464
+ are already runtime dependencies of opentel-mcp itself (`@opentelemetry/sdk-metrics`
465
+ as of v0.15.0, for the dev-mode metrics path below; the other two for
466
+ `setupNodeSdk: true`'s dev tracing path, same as before), so no extra
467
+ install is needed for those three — only the OTLP metrics exporter above
468
+ needs installing yourself.
469
+
470
+ #### Dev-mode metrics (v0.15.0+)
471
+
472
+ `setupNodeSdk: true` now also stands up an owned `MeterProvider` — the
473
+ same dev-friendly, zero-config posture tracing has always had under this
474
+ flag, extended to metrics (ADR 023, `docs/adr/023-dev-mode-metrics.md`).
475
+ This is a dev-mode-only exception to the API-only framing above, the same
476
+ way `StderrSpanExporter` (ADR 003) is a dev-mode-only exception for
477
+ tracing — `setupNodeSdk: false` (the default, and the production posture)
478
+ is completely unaffected; metrics stay exactly as API-only as they are
479
+ today.
480
+
481
+ What it does, under `setupNodeSdk: true` and `enableMetrics` (default
482
+ `true`, same flag that already opts out of metrics generally):
483
+
484
+ - Builds a `MeterProvider` with a `PeriodicExportingMetricReader` exporting
485
+ to stderr every 5 seconds (not the SDK's own 60-second default — a
486
+ quickstart user making a few tool calls should see output in a few
487
+ seconds, not up to a minute later), via a small custom exporter
488
+ (`src/exporters/stderr-metrics.js`) — same "stderr, never stdout"
489
+ reasoning as `StderrSpanExporter` (ADR 003): a `StdioServerTransport`
490
+ server's JSON-RPC stream lives on stdout, so diagnostic output of any
491
+ kind must go to stderr instead.
492
+ - Prints one line per instrument × attribute-set, not a JSON dump:
493
+ counters as `name{attrs} = <value>`, histograms as
494
+ `name{attrs} count=<n> avg=<sum/count><unit>` — see the quickstart's
495
+ "See it working" section above for a real example.
496
+ - Shares the exact same `Resource` (service.name, and the pricing-staleness
497
+ attribute when applicable) the dev `TracerProvider` already builds, so
498
+ the two signals can never carry different `service.name` values in dev
499
+ mode.
500
+ - **If a `MeterProvider` is already registered globally** — your own, or
501
+ anything else's — this is a complete no-op: nothing is overridden, no
502
+ exception is thrown, and a single `diag.warn` (once per process) says
503
+ so. `setupMeter()` immediately afterward continues to use whatever was
504
+ already registered, exactly as it does today.
505
+ - Extends `server.shutdown()` (already attached under `setupNodeSdk: true`)
506
+ to also flush and shut down the `MeterProvider` it registered, alongside
507
+ the existing `TracerProvider` shutdown. As of v0.16.0 this runs
508
+ automatically when the process stops — see "Flushing on exit" below.
509
+
510
+ ### Flushing on exit (`flushOnExit`, v0.16.0)
511
+
512
+ Under `setupNodeSdk: true`, buffered spans and dev-mode metrics are
513
+ flushed automatically when the process stops. It's on by default; pass
514
+ `flushOnExit: false` to turn it off. MCP clients built on the SDK's stdio
515
+ transport stop a server by closing stdin, then sending SIGTERM 2 seconds
516
+ later, then SIGKILL; this covers the first two:
517
+
518
+ | Event | What happens |
519
+ | --- | --- |
520
+ | stdin closes and nothing else keeps the process alive | flushed on `beforeExit`; the process exits with its own exit code |
521
+ | `SIGTERM` / `SIGINT` | flushed, then the same signal is re-raised so the process still dies by it, unless your code has its own listener for that signal, in which case yours decides |
522
+ | `process.exit()` | **not covered**: call `await server.shutdown()` before exiting |
523
+ | `SIGKILL` | can't be intercepted |
524
+
525
+ The flush waits at most 1000 ms (`flushOnExit: { timeoutMs }` can only
526
+ lower that), so a stuck exporter can't keep a stopping server alive.
527
+ `server.shutdown()` is safe to call yourself as well: it runs once, however
528
+ many times it's called. With `setupNodeSdk: false` this option does
529
+ nothing — your application owns its providers and its shutdown. Design:
530
+ [ADR 024](../../docs/adr/024-flush-on-exit.md).
531
+
532
+ `@opentelemetry/sdk-metrics` is a real (non-dev) dependency of opentel-mcp
533
+ as of this feature — not an optional peer, not a dynamic import with a
534
+ fallback path. See ADR 023's "Dependency strategy" section for the full
535
+ reasoning: its own transitive dependencies were already in this package's
536
+ tree via `@opentelemetry/resources`/`@opentelemetry/sdk-trace-node`, so
537
+ this adds no new package beyond itself.
341
538
 
342
539
  #### Using the Prometheus exporter instead of OTLP — `service.name` needs an extra option
343
540
 
@@ -2121,6 +2318,7 @@ so they're spelled out instead of forced into one bucket.
2121
2318
  | `mcp.tool.pricing_status` | span attr, metric label | **blend — library-computed, depends on tool + host** | which of three enum values depends on a tool-controlled model name resolving (or not) in a library-default-or-host-overridden pricing table |
2122
2319
  | `mcp.failure.fingerprint` / `.signature` / `.category` / `.origin` / `.channel` | span attr (2 also metric labels) | library-computed | hashes/enums — the governed core this project has kept clean since v0.4.0 |
2123
2320
  | `mcp.failure.error_class` | span attr | **blend — tool-controlled, capped only** | `err.name`, length-capped at 128 characters but, unlike everything hashed into the fingerprint, *not* pattern-scrubbed — a documented, deliberate tradeoff (`docs/known-gaps.md` entry 10) |
2321
+ | `mcp.failure.unactionable` / `.content_length_bucket` | span attr | library-computed | a boolean and a 5-value enum derived from the trimmed text LENGTH and non-text item COUNT of an `isError: true` result — no text is read for anything but its length (ADR 025, v0.16.0) |
2124
2322
  | `mcp.failure.validation_paths` | span attr | mostly library-computed (gated) | one legacy Zod-issue-rendering path can still carry a tool-controlled dynamic object key, now redacted to a `<KEY>` placeholder rather than closed structurally (`docs/known-gaps.md` entry 10, item 3) |
2125
2323
  | `exception.type` (event) | event attr | tool-controlled | length-capped only under `'normalized'` mode; uncapped under `'full'` (the native SDK call) |
2126
2324
  | `exception.message` / `.stacktrace` (event), status message | event attr + status | **blend — layered** | raw tool content under `'full'`; this library's `normalizeMessage()`/`parseAndNormalizeStack()` transform of that same content under `'normalized'`; optionally a host-supplied `errorRecording.redactor`'s output feeding *that*, first (v0.14.0) — the most layered-provenance attribute in the system |
@@ -2621,7 +2819,7 @@ pragmatic choice rather than a spec-pure one.
2621
2819
  revision 2026-07-28, v0.10.0+; see "MCP v2 support" above for what's
2622
2820
  covered and `docs/known-gaps.md` entries 6 and 8 for what isn't yet)
2623
2821
  - @opentelemetry/api ^1.9.0
2624
- - 948 tests, 944 passing + 4 intentionally skipped (`npm test`) — see `test/`
2822
+ - 1017 tests, 1013 passing + 4 intentionally skipped (`npm test`) — see `test/`
2625
2823
  - `npm run typecheck` (`tsc --noEmit`) type-checks the public `.d.ts`
2626
2824
  surface (`src/index.d.ts` and friends) — see CONTRIBUTING.md
2627
2825
 
@@ -2701,6 +2899,17 @@ pragmatic choice rather than a spec-pure one.
2701
2899
  narrower thing still open, tracked in `docs/known-gaps.md` entry 6:
2702
2900
  `thrashSessionState`'s session-awareness memory isn't shared across v2's
2703
2901
  per-request calls yet, only the fallback id itself.
2902
+ - v0.15.0: "Make it visible" ✓ — a recent audit found that this library's
2903
+ real depth (12 metrics, two-SDK support, a working local dashboard) was
2904
+ mostly invisible to a first-time user. Two fixes, no instrumentation
2905
+ changes: publishing `opentel-mcp-ui` (ADR 022,
2906
+ `docs/adr/022-publish-ui.md`) — previously a working but unpublished,
2907
+ `"private": true` scaffold, now `npx opentel-mcp-ui --demo`-able from a
2908
+ clean machine — and dev-mode metrics under `setupNodeSdk: true` (ADR
2909
+ 023, `docs/adr/023-dev-mode-metrics.md`, see "Dev-mode metrics" above),
2910
+ closing the gap where the documented 30-second quickstart emitted
2911
+ stderr spans but none of the 12 `mcp.tool.*`/`mcp.tool.loop.*`/
2912
+ `mcp.tool.schema_drift.*` metrics it leads with.
2704
2913
  - Future: failure clustering + regression detection; recovery hints;
2705
2914
  root-cause chaining across parent spans; alignment with the OTel GenAI
2706
2915
  SIG's MCP semantic conventions when published
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentel-mcp",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "One-line OpenTelemetry instrumentation for Model Context Protocol (MCP) servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -69,13 +69,14 @@
69
69
  "dependencies": {
70
70
  "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
71
71
  "@opentelemetry/resources": "^2.9.0",
72
+ "@opentelemetry/sdk-metrics": "^2.9.0",
72
73
  "@opentelemetry/sdk-trace-node": "^2.9.0"
73
74
  },
74
75
  "devDependencies": {
75
- "@modelcontextprotocol/server": "^2.0.0",
76
+ "@modelcontextprotocol/sdk": "1.32.1",
77
+ "@modelcontextprotocol/server": "2.3.1",
76
78
  "@opentelemetry/api": "^1.9.0",
77
79
  "@opentelemetry/context-async-hooks": "^2.9.0",
78
- "@opentelemetry/sdk-metrics": "^2.9.0",
79
80
  "@opentelemetry/sdk-trace-base": "^2.9.0",
80
81
  "@vitest/coverage-v8": "^2.1.9",
81
82
  "js-yaml": "^4.3.1",
package/src/attributes.js CHANGED
@@ -76,6 +76,29 @@ export const MCP_METHOD_NAME_TOOLS_CALL = 'tools/call';
76
76
  /** Well-known mcp.method.name value for a tools/list request (ADR 010, schema drift detection). */
77
77
  export const MCP_METHOD_NAME_TOOLS_LIST = 'tools/list';
78
78
 
79
+ // ADR 026 (v0.16.0, opt-in `coverage`): the five resource/prompt methods.
80
+ // mcp.method.name only ever takes these fixed string constants, never a
81
+ // value read from a request, so its value space stays bounded (7 total).
82
+
83
+ /** Well-known mcp.method.name value for a resources/read request (ADR 026). */
84
+ export const MCP_METHOD_NAME_RESOURCES_READ = 'resources/read';
85
+ /** Well-known mcp.method.name value for a resources/list request (ADR 026). */
86
+ export const MCP_METHOD_NAME_RESOURCES_LIST = 'resources/list';
87
+ /** Well-known mcp.method.name value for a resources/templates/list request (ADR 026). */
88
+ export const MCP_METHOD_NAME_RESOURCES_TEMPLATES_LIST = 'resources/templates/list';
89
+ /** Well-known mcp.method.name value for a prompts/get request (ADR 026). */
90
+ export const MCP_METHOD_NAME_PROMPTS_GET = 'prompts/get';
91
+ /** Well-known mcp.method.name value for a prompts/list request (ADR 026). */
92
+ export const MCP_METHOD_NAME_PROMPTS_LIST = 'prompts/list';
93
+
94
+ /**
95
+ * gen_ai.prompt.name — the prompt a prompts/get span requested (ADR 026).
96
+ * Span attribute only, never a metric label. Capped at
97
+ * MAX_PROMPT_NAME_LENGTH, since the value comes from the request.
98
+ */
99
+ export const ATTR_GEN_AI_PROMPT_NAME = 'gen_ai.prompt.name';
100
+ export const MAX_PROMPT_NAME_LENGTH = 128;
101
+
79
102
  // --- Custom (non-spec) attributes ---
80
103
 
81
104
  /**