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 +220 -0
- package/README.md +484 -49
- package/package.json +2 -2
- package/src/attributes.js +77 -4
- package/src/error-recording/config.js +84 -11
- package/src/error-recording/redactor.js +140 -0
- package/src/error-recording/types.d.ts +93 -12
- package/src/exporters/stderr-metrics.js +142 -0
- package/src/fingerprint/compose.js +10 -50
- package/src/fingerprint/normalize/exception.js +111 -0
- package/src/fingerprint/normalize/message.js +5 -1
- package/src/index.d.ts +63 -18
- package/src/instrument.js +208 -38
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
|