opentel-mcp 0.13.0 → 0.15.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,225 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0
4
+
5
+ "Make it visible" — no instrumentation changes, no `tools/call` hot-path
6
+ changes. Phase 1 (ADR 022, `docs/adr/022-publish-ui.md`): publishes
7
+ `opentel-mcp-ui`, previously a working but `"private": true`, unpublished
8
+ scaffold with zero mentions in this README. See
9
+ [`opentel-mcp-ui`'s own CHANGELOG](https://www.npmjs.com/package/opentel-mcp-ui)
10
+ for what changed in that package. Phase 2 (ADR 023,
11
+ `docs/adr/023-dev-mode-metrics.md`): dev-mode metrics under
12
+ `setupNodeSdk: true` — see "Added" below.
13
+
14
+ ### Added
15
+
16
+ - README: a "See it locally" section near the top, pointing at
17
+ `npx opentel-mcp-ui --demo` — this library had no documented way for a
18
+ new reader to see its own output before installing anything.
19
+ - `setupNodeSdk: true` now also stands up an owned `MeterProvider` (ADR
20
+ 023): a `PeriodicExportingMetricReader` exporting all 12 `mcp.tool.*`/
21
+ `mcp.tool.loop.*`/`mcp.tool.schema_drift.*` instruments to stderr every
22
+ 5 seconds, via a new `StderrMetricExporter` (compact
23
+ `name{attrs} = value` lines, never a stdout-writing JSON dump — same
24
+ ADR 003 reasoning `StderrSpanExporter` already follows). Gated
25
+ additionally by `enableMetrics` (default `true`, unchanged option).
26
+ Closes the gap where the documented 30-second quickstart emitted
27
+ stderr spans but none of its 12 metrics. `setupNodeSdk: false` (the
28
+ default, and the production posture) is completely unaffected — every
29
+ new code path is gated behind the same existing `setupNodeSdk === true`
30
+ check `StderrSpanExporter` already uses.
31
+ - If a `MeterProvider` is already registered globally when this runs
32
+ (your own, or anything else's), this is a complete no-op: nothing is
33
+ overridden, no exception is thrown, and a single `diag.warn` (once
34
+ per process, not once per `instrumentMcpServer()` call) names the
35
+ situation. Detected via `@opentelemetry/api`'s own documented
36
+ `boolean` return from `setGlobalMeterProvider()` — not by inspecting
37
+ internals. The abandoned candidate provider built before that check
38
+ is shut down immediately (fire-and-forget), closing a leak where its
39
+ own periodic reader would otherwise tick forever, unused, in the
40
+ background.
41
+ - `server.shutdown()` (already attached under `setupNodeSdk: true`) now
42
+ also flushes and shuts down this `MeterProvider`, alongside the
43
+ existing `TracerProvider` shutdown it already performed.
44
+ - `StderrMetricExporter` only reprints a metric when its value has
45
+ actually changed since the last export. Every instrument here is
46
+ CUMULATIVE, so without this a single early tool call would otherwise
47
+ reprint two unchanged lines every 5 seconds for the rest of the
48
+ process's life — confirmed empirically before this fix shipped, not
49
+ assumed.
50
+
51
+ ### Changed
52
+
53
+ - **`@opentelemetry/sdk-metrics` is now a runtime (`dependencies`, not
54
+ `devDependencies`) dependency of `opentel-mcp`, installed for every
55
+ user — including under `setupNodeSdk: false`, which never executes any
56
+ code from it.** Needed at import time for the feature above; moving it
57
+ to `dependencies` means `npm install opentel-mcp` always pulls it in,
58
+ regardless of whether a given deployment ever sets `setupNodeSdk: true`.
59
+ Adds no new transitive dependency: its own two dependencies
60
+ (`@opentelemetry/core`, `@opentelemetry/resources`) were already in
61
+ this package's tree via `@opentelemetry/sdk-trace-node`/the existing
62
+ `@opentelemetry/resources` dependency. See ADR 023's "Dependency
63
+ strategy" section for the full reasoning, including why an optional-peer
64
+ + dynamic-import approach was considered and rejected.
65
+
66
+ ### Fixed
67
+
68
+ - The example servers (`examples/hello-server`, `examples/hello-mcpserver`)
69
+ never showed a metrics line in their own READMEs' "What you'll see": a
70
+ single piped `tools/call` request exits the process before the 5-second
71
+ export interval. Both now flush once via `process.on('beforeExit', ...)`
72
+ right before the process would otherwise exit — confirmed
73
+ `StdioServerTransport` does not call its own `close()`/`onclose` on
74
+ stdin EOF, so `beforeExit` is the one hook that still allows the async
75
+ flush to complete first.
76
+ - Documentation only, no code/behavior changes:
77
+ - README claimed "Four `mcp.tool.*` metrics" in a way that read as the
78
+ library's total metric count. Corrected: four is this section's own
79
+ base call/duration metrics; twelve exist in total across cost/token
80
+ attribution (2), Agent Thrash Detection (5), and schema drift (1),
81
+ each already documented in their own sections.
82
+ - README's test count was stale at every point it was checked this
83
+ release; now 1017 tests, 1013 passing + 4 intentionally skipped.
84
+ - README's SigNoz metrics-wiring example described
85
+ `@opentelemetry/sdk-metrics` as a host-app dependency opentel-mcp
86
+ "doesn't bundle" — true before this release, no longer true now that
87
+ it's a real dependency (see "Changed" above).
88
+
89
+ ### CI
90
+
91
+ - `dependency-audit` (`.github/workflows/ci.yml`) no longer audits this
92
+ monorepo's own shared, hoisted `node_modules` — `npm audit`, even with
93
+ `--omit=dev` and run from inside `packages/core/`, scans the entire
94
+ installed tree regardless of cwd or `--workspace` (confirmed
95
+ empirically), so it was reporting findings from `examples/*`'s real
96
+ `@modelcontextprotocol/sdk` dependency (express/hono/ajv transitives)
97
+ as if they were this library's own. Now packs and installs each of
98
+ `packages/core`/`packages/ui` into its own clean, throwaway project —
99
+ the exact tree a real `npm install` produces — and audits that instead.
100
+
101
+ ## 0.14.0
102
+
103
+ Adds `errorRecording.redactor`: a host-supplied hook for content
104
+ `normalizeMessage()`/`parseAndNormalizeStack()` don't recognize — a
105
+ proprietary API key format, an internal account id shape, a customer name
106
+ in prose or in a multi-tenant stack frame path. Full design: ADR 020
107
+ (`docs/adr/020-redactor-hook.md`). Ships in the same release as the
108
+ 0.13.1 fix/docs items below (0.13.1 itself was never tagged/published —
109
+ see that section's own note), but is a logically separate, purely
110
+ additive change from both of them; kept in its own section here rather
111
+ than folded into 0.13.1's for that reason.
112
+
113
+ ### Added — `errorRecording.redactor` hook (ADR 020)
114
+
115
+ - New optional `errorRecording.redactor` field: a synchronous
116
+ `(input: { message, stack }) => { message, stack }` function, consulted
117
+ only under `mode: 'normalized'`, that runs on the raw, uncoerced
118
+ `message`/`stack` — via the same coercion this library's own pipeline
119
+ already uses — **before** `normalizeMessage()`/`parseAndNormalizeStack()`
120
+ ever see them, never the reverse. This library's own scrubbing still
121
+ runs second, over your redactor's output, as a defense-in-depth
122
+ backstop.
123
+ - Configuring a redactor alongside `mode: 'full'` or `'none'` is accepted
124
+ but never invoked — those two modes' entire meaning is "byte-identical,
125
+ regardless of what else is configured" — and produces a one-time
126
+ `diag.warn()` at `instrumentMcpServer()` setup naming the no-op, since
127
+ forgetting to also flip `mode` is an easy, otherwise-silent
128
+ misconfiguration.
129
+ - A redactor that throws, returns a non-string `message`, or returns a
130
+ `stack` that's neither a string nor `undefined` falls back to
131
+ `'none'`-equivalent span output for that one event — status set, no
132
+ `exception` event — **never** to raw/unredacted content, plus a
133
+ one-time, content-free `diag.warn()` naming the failure shape (never
134
+ the message/stack content itself). The returned `message`/`stack` are
135
+ also length-capped defensively before use (reusing
136
+ `normalizeMessage()`'s existing 2048-character limit).
137
+ - **Never reaches `computeFingerprint()`'s hash input, in any
138
+ configuration.** `mcp.failure.fingerprint`/`signature` are computed
139
+ from the real, unmodified thrown error, identically whether or not a
140
+ redactor is configured — a deliberate, non-negotiable design decision
141
+ (ADR 020 Decision 3): the fingerprint is a SHA-256 hash, never a
142
+ plaintext channel, so redacting it would buy no privacy while tying
143
+ fingerprint stability to unversioned host code. Any existing dashboard
144
+ filter or alert keyed on `mcp.failure.fingerprint` keeps working
145
+ unchanged after adding a redactor.
146
+ - **Timing caveat, not just a telemetry one:** the redactor runs
147
+ synchronously, inline, on the error path, before the span ends — there
148
+ is no timeout, and JavaScript's single-threaded execution model means
149
+ one can't be added without a `worker_threads`/`vm.Script` boundary this
150
+ feature deliberately doesn't pay for. A slow redactor (catastrophic
151
+ regex backtracking, an accidental blocking call) adds directly to that
152
+ tool call's own response latency, not just to what shows up in traces.
153
+ See the README's "The redactor hook" section, "Timing," for the full
154
+ writeup, and ADR 020's own "Timing" section for why this is an accepted
155
+ risk rather than a solved one.
156
+ - No environment-variable equivalent — a function can't be expressed as
157
+ an `OTEL_MCP_*` string, same as `costTracking.extractor`/
158
+ `thrashDetection`'s classifier-shaped options already are.
159
+ - New exported types `ErrorRecordingRedactor` and
160
+ `ErrorRecordingRedactorFields` (`src/error-recording/types.d.ts`,
161
+ re-exported from the package root) — a consumer can type a redactor
162
+ function on its own, the same way `UsageExtractor`/`Classifier` already
163
+ let a consumer type an extractor/classifier independently of the option
164
+ object that carries it.
165
+ - Purely additive and default-off: a deployment that never sets
166
+ `errorRecording.redactor` observes zero behavior change.
167
+
168
+ ## 0.13.1
169
+
170
+ Never tagged or published as its own release — ships bundled into
171
+ 0.14.0 above instead, since the redactor work started before this patch
172
+ went out the door. Kept as its own section here (rather than merged into
173
+ 0.14.0's) because both items below are logically independent of the
174
+ redactor: one's a bug fix to existing `'normalized'`-mode behavior, the
175
+ other's a documentation-only correction, and neither has anything to do
176
+ with the new hook.
177
+
178
+ ### Fixed — `'normalized'` mode dropped `exception.message`/status message for non-`Error` throws
179
+
180
+ - Before this fix, `recordThrownException()`'s `'normalized'` branch
181
+ independently re-derived `message`/`stack` via a plain `err?.message`/
182
+ `err?.stack` read — which silently returned `undefined` for a thrown
183
+ **string** or a plain non-`Error` object (neither has a `.message`
184
+ property the way a real `Error` instance does), while
185
+ `computeFingerprint()`'s own, richer `coerceError()` correctly
186
+ recognized those same shapes and still produced a real
187
+ `mcp.failure.fingerprint`. Net effect: a thrown string or non-`Error`
188
+ throw got real fingerprinting but a completely empty `exception.message`
189
+ / status message on the span under `'normalized'` mode — same `err`,
190
+ two independent readers, two different answers, silently.
191
+ - Fixed by extracting the shared `normalizeException()`/`coerceError()`
192
+ pair into their own module (`src/fingerprint/normalize/exception.js`)
193
+ and routing **both** `computeFingerprint()` and `recordThrownException()`
194
+ through the same one computation for the same `err` — the span's
195
+ exception content and the fingerprint's hashed inputs can no longer
196
+ independently drift apart, by construction, not by convention. This is
197
+ the same consolidation ADR 020 (the redactor's own design doc, above)
198
+ repeatedly cites as precedent for keeping the fingerprint path
199
+ structurally isolated from the span-writing path going forward.
200
+ - `'full'` and `'none'` modes were never affected — this bug was specific
201
+ to `'normalized'` mode's own inline coercion.
202
+
203
+ ### Documentation — `errorRecording.mode` scoping correction
204
+
205
+ - Clarified that `errorRecording.mode` controls only the span this
206
+ library creates for the current `tools/call`/`tools/list` — it has no
207
+ effect on exception content any other instrumentation in the same
208
+ process (an APM agent, HTTP or framework auto-instrumentation, anything
209
+ else wrapping the handler) independently records onto its own span for
210
+ the same rethrown error. Since no mode mutates the original `err`, that
211
+ raw content can still land in the same trace, one span up, regardless
212
+ of mode — including `'none'`, which was previously worded in a way that
213
+ could read as a trace-wide guarantee rather than a span-scoped one.
214
+ Confirmed empirically (a real ambient context manager, an outer span
215
+ recording the rethrown error, `InMemorySpanExporter`) — see
216
+ `docs/known-gaps.md` entry 10's 2026-09-01 update for the full
217
+ reproduction and reasoning. README's "Error recording" and "What this
218
+ library records" sections updated accordingly. No code changed — the
219
+ underlying behavior (and the exposure at the default `'full'` mode) was
220
+ already accurate and unchanged; only the description of what
221
+ `errorRecording.mode` scopes to was incomplete.
222
+
3
223
  ## 0.13.0
4
224
 
5
225
  Closes the two open items from `docs/known-gaps.md` entry 10 (a