opentel-mcp 0.9.0 → 0.11.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,229 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ **⚠️ Type change, not a runtime behavior change — read this first.**
6
+ `ModelPricing` is now a discriminated union
7
+ (`{pricingKind: 'chat', inputPer1M, outputPer1M, currency} |
8
+ {pricingKind: 'embedding', inputPer1M, currency}`) instead of a single
9
+ shape with both token fields always present. A TypeScript consumer with
10
+ an existing custom `pricingTable`/`pricing` object typed against the old
11
+ shape will see a compile error requiring `pricingKind` on each entry.
12
+ **Runtime behavior for those same objects is unchanged**: `calculateCost()`
13
+ treats a missing or unrecognized `pricingKind` as `'chat'`, exactly the
14
+ behavior every pre-v0.11.0 entry already had. See ADR 016
15
+ (`docs/adr/016-pricing-override-and-staleness.md`) point 1.
16
+
17
+ ### Added — Pricing table override and staleness signalling (Phase 1 of 2)
18
+
19
+ `DEFAULT_PRICING` had the same disease this whole library exists to fix
20
+ elsewhere: a hardcoded snapshot with no signal when it's wrong or stale.
21
+ This release closes that. Full design: ADR 016
22
+ (`docs/adr/016-pricing-override-and-staleness.md`).
23
+
24
+ - **Embedding model support.** `DEFAULT_PRICING` gains OpenAI
25
+ `text-embedding-3-small`/`text-embedding-3-large`/`text-embedding-ada-002`,
26
+ Cohere `cohere-embed-v3`, and Bedrock `amazon-titan-embed-v2`.
27
+ Embeddings are input-token-only — rather than modeling that as
28
+ `outputPer1M: 0` (indistinguishable from a data-entry bug), a new
29
+ `pricingKind: 'chat' | 'embedding'` discriminator makes it explicit; an
30
+ `'embedding'` entry has no `outputPer1M` field at all, and
31
+ `calculateCost()` never reads `outputTokens` for one (still validated as
32
+ a non-negative finite number, just never charged for).
33
+ - **`costTracking.pricing`: per-model merge over defaults.** New option,
34
+ a *partial* pricing table merged per-model OVER `pricingTable ??
35
+ DEFAULT_PRICING` — each key you supply replaces that model's entire
36
+ pricing entry, every model you don't name is untouched. This is now the
37
+ recommended way to correct a stale price or add a model
38
+ `DEFAULT_PRICING` doesn't know about, without spreading the whole
39
+ default table by hand (the old workaround the README used to teach).
40
+ `costTracking.pricingTable` keeps its existing full-replace behavior,
41
+ unchanged, for the narrower "I want only my own models" case — see ADR
42
+ 016 point 2 for why both exist.
43
+ - **`mcp.tool.pricing_status` span + metric attribute.** One of `"known"`
44
+ | `"unknown"` | `"user_override"`, set whenever token usage was
45
+ extracted at all — even with no model detected (`"unknown"` in that
46
+ case), unlike the existing model/cost attributes. Added to the
47
+ `mcp.tool.tokens.total` / `mcp.tool.cost.total` metrics too, so a
48
+ dashboard can compute "% of tokens/spend unpriced" as a direct
49
+ aggregation instead of inferring it from missing data. Provenance-based:
50
+ `"user_override"` means the model's key came from your
51
+ `pricing`/`pricingTable`, regardless of whether the numbers you supplied
52
+ happen to match `DEFAULT_PRICING`'s own entry.
53
+ - **Staleness signalling.** `DEFAULT_PRICING_LAST_VERIFIED` (also
54
+ exported) names the table's last-checked date; once it's more than 90
55
+ days old, `instrumentMcpServer()` fires a one-time `diag.warn()`, and,
56
+ when `setupNodeSdk: true`, also attaches an
57
+ `mcp.pricing.default_table_last_verified` resource attribute. Both only
58
+ fire when `DEFAULT_PRICING` is actually contributing to the effective
59
+ table — a caller who fully replaced it via `pricingTable` isn't using
60
+ our defaults, so a warning about them would be misleading.
61
+ `isDefaultPricingStale(now?, thresholdDays?)` (also exported) is the
62
+ pure function behind the warning, for callers who want to check it
63
+ themselves.
64
+ - **Bedrock region caveat, documented not modeled.** Bedrock pricing
65
+ varies by region; `DEFAULT_PRICING`'s Bedrock entries (Nova, and the new
66
+ Titan embedding entry) assume us-east-1 list price and the table is not
67
+ region-keyed — no tool-result usage shape this package recognizes
68
+ carries a region signal to key a lookup on. Documented loudly in the
69
+ README and in `pricing.js`; override via `costTracking.pricing` for a
70
+ different region. See ADR 016 point 5.
71
+ - `calculateCost()` gained defensive validation for malformed pricing
72
+ entries (missing/negative/non-numeric `inputPer1M`/`outputPer1M`),
73
+ since `pricing`/`pricingTable` now make it reachable with
74
+ caller-supplied shapes it previously never had to distrust — degrades
75
+ to `null`, same as an unknown model, never throws.
76
+
77
+ ### Added — W3C Trace Context propagation over MCP `_meta` (Phase 2 of 2, server-side only)
78
+
79
+ The most-complained-about gap in agent observability: an agent's own
80
+ trace (LangGraph or otherwise) and the MCP server's trace for the tool
81
+ call it made were always two disconnected traces, with no edge between
82
+ them. Full design, including the sampling and conflicting-context
83
+ decisions below: ADR 017 (`docs/adr/017-trace-context-propagation.md`).
84
+
85
+ - **`tools/call` requests carrying a valid W3C `traceparent` in
86
+ `params._meta` now become a child of the calling agent's own span**,
87
+ joining what were two disconnected traces into one — under both MCP v1
88
+ and v2, with zero configuration and no new option. `tracestate` is
89
+ propagated too, when present. Works for any client already emitting
90
+ `traceparent` via a standard OTel SDK's `propagation.inject()` in any
91
+ language — this isn't Node/JS-specific on the client side, only on
92
+ which side of the wire this release implements.
93
+ - **The upstream sampling decision is honored automatically, by
94
+ construction, with no sampling logic written for this feature**: the
95
+ extracted `SpanContext` is marked `isRemote: true` with the real parsed
96
+ `traceFlags`, which is exactly what the SDK's own default
97
+ `ParentBasedSampler` already keys its remote-parent decision off of. A
98
+ not-sampled upstream `traceparent` means this tool-call span is not
99
+ recorded or exported, matching the calling agent's own choice — see the
100
+ ADR's "Sampling" section for why forcing sampling regardless was
101
+ considered and rejected.
102
+ - **A `_meta`-extracted context always replaces, never merges with, an
103
+ already-active local context** (e.g. an ambient HTTP-server span from
104
+ auto-instrumentation on a Streamable HTTP transport) — the message-level
105
+ `_meta` context is the semantically correct parent for one tool call,
106
+ full stop, regardless of what transport-level span it happened to
107
+ arrive inside. See the ADR's "Conflicting `_meta.traceparent`" section.
108
+ - **Absent, malformed, or unparseable `_meta`/`traceparent` produces
109
+ behavior that is byte-identical to pre-v0.11.0** — not merely
110
+ equivalent to it: confirmed by reading both `NoopTracer` and the real
111
+ SDK `Tracer`'s own `startActiveSpan()` fallback (`ctx ?? context.active()`),
112
+ which is exactly what this feature's `extractTraceContext()` returns
113
+ for every case that isn't a valid `traceparent`. No `diag.warn()` for
114
+ the common "client doesn't send `_meta.traceparent`" case — see the
115
+ ADR's "no warn spam" constraint.
116
+ - **Zero new dependencies.** `@opentelemetry/core`'s
117
+ `W3CTraceContextPropagator` was the obvious reference implementation
118
+ and was deliberately not taken as a dependency, per
119
+ `CONTRIBUTING.md`'s "no new dependencies without discussion first" —
120
+ everything needed except the traceparent regex itself (~10 lines,
121
+ matching `@opentelemetry/core`'s own validation field-for-field) was
122
+ already available from `@opentelemetry/api`, already a peer dependency
123
+ — including `createTraceState()`, a fully spec-validated `tracestate`
124
+ parser. Full reasoning: ADR 017's "No new dependency" section.
125
+ - **Server-side extraction only.** The client-side shim that would let a
126
+ Node/Python agent framework *set* `_meta.traceparent` on outgoing calls
127
+ is explicitly out of scope for this phase — extraction is independently
128
+ useful today, for free, to any client whose own tooling already sets
129
+ `_meta` in this shape. Tracked as future work, not implied as solved.
130
+
131
+ Also fixed in this release: two places (`index.d.ts`'s
132
+ `instrumentMcpServer()` docblock, and this file's own v0.10.0 entry
133
+ below) still described `docs/known-gaps.md` entries 6/7/8 using language
134
+ that read as still-open, or as scoped out of v0.10.0 — both were stale.
135
+ Entries 7 and 8 have been fully fixed since v0.10.0 with no open caveats;
136
+ entry 6's fallback-session-id half is fixed too, narrowed to a smaller,
137
+ genuinely-still-open remainder (see the corrected v0.10.0 entry below and
138
+ `index.d.ts`'s updated docblock for the accurate, current accounting).
139
+
140
+ ## 0.10.0
141
+
142
+ **⚠️ Behavior change, unrelated to the feature below — read this first.**
143
+ `instrumentMcpServer()` now throws for a server object it cannot
144
+ confidently wrap, instead of silently instrumenting nothing.
145
+ `detectServerKind()` (`src/instrument.js`) previously accepted any
146
+ `McpServer`-shaped object whose `.server` merely *had* a
147
+ `setRequestHandler` method — it now additionally requires `.server
148
+ instanceof <Server>` for a real, recognized SDK class. An object that
149
+ satisfies the outer shape but fails that check now throws a new,
150
+ specific error (`UNWRAPPABLE_MCPSERVER_ERROR` — names what was detected
151
+ and the plausible causes: a duplicate/mismatched SDK install, an SDK not
152
+ resolvable from this package's own location, or an unsupported SDK) at
153
+ `instrumentMcpServer()` call time, rather than succeeding and producing
154
+ zero telemetry. This closes a confirmed gap (`docs/known-gaps.md` entry
155
+ 7, now marked fixed): an `@modelcontextprotocol/server` (MCP v2) object
156
+ passed to a pre-0.10.0 `instrumentMcpServer()` satisfied the old, looser
157
+ check and appeared to instrument successfully — `getThrashSummary`/
158
+ `getObservationState` attached, no error — while producing zero spans,
159
+ zero metrics, and zero fingerprinting for every tool call. No escape
160
+ hatch was added; see ADR 015 (`docs/adr/015-mcp-v2-support.md`) for the
161
+ full argument against one. **If you're seeing this new error on upgrade**
162
+ and you believe your object genuinely is a real `Server`/`McpServer`
163
+ instance, check for a duplicate/mismatched install of whichever SDK it
164
+ came from (`npm dedupe`, or check for multiple installed copies) — a real
165
+ v1 or v2 `Server`/`McpServer` from a single, consistently-resolved SDK
166
+ install is unaffected by this change.
167
+
168
+ ### Added — `@modelcontextprotocol/server` (MCP v2, protocol revision 2026-07-28) support
169
+
170
+ Both the original `@modelcontextprotocol/sdk` ("v1") and the new
171
+ `@modelcontextprotocol/server` ("v2") now work with `instrumentMcpServer()`
172
+ — two separate, OPTIONAL peer dependencies (install whichever one(s) you
173
+ actually use; `package.json`'s `peerDependenciesMeta` marks both
174
+ `optional: true`, verified against real, clean external installs with
175
+ only one, the other, or neither installed — not just `package.json`
176
+ syntax). Same `Server`/`McpServer` API shapes as v1; detection and
177
+ wrapping happen automatically, resolved once per `instrumentMcpServer()`
178
+ call by which SDK the object actually came from. Full design and
179
+ Phase-by-phase implementation notes: ADR 015
180
+ (`docs/adr/015-mcp-v2-support.md`).
181
+
182
+ What works the same as v1: spans, standard attributes (including
183
+ `jsonrpc.request.id`, now read from v2's `ctx.mcpReq.id`), deep failure
184
+ fingerprinting, and `mcp.failure.channel`/`mcp.failure.validation_paths`
185
+ classification (`channel.js`/`validation-paths.js` both gained a
186
+ v2-specific code path — the "MCP error N:" wrapper v1 disguises errors
187
+ with doesn't exist in v2, and v2's rendered validation-issue text uses a
188
+ third, distinct format from either of v1's two).
189
+
190
+ **v2's own `createMcpHandler`/`serveStdio` construct a fresh `Server`/
191
+ `McpServer` per request by default (a factory function you provide), not
192
+ once at process start.** `instrumentMcpServer()` needs to run *inside*
193
+ that factory, on every invocation — see the README's new "MCP v2 support"
194
+ section for a worked example. `instanceKey` (v0.9.0) is the existing
195
+ mechanism for sharing tracker state across those repeated calls; nothing
196
+ new was added for this, since ADR 012's original design already covers
197
+ this exact deployment shape, v2 just makes it the default instead of an
198
+ edge case.
199
+
200
+ **Correction (recorded here rather than silently edited): both gaps below
201
+ were actually closed in this same v0.10.0 release, not left open.** The
202
+ paragraph originally here said entries 6 and 8 (`docs/known-gaps.md`)
203
+ were scoped out of this round — true of the round that produced the text
204
+ above, not of what actually shipped. A follow-up investigation, completed
205
+ before v0.10.0 was cut, folded both fixes back in: `isSingleConnectionTransport()`
206
+ no longer misclassifies the transport `createMcpHandler` builds internally
207
+ (entry 8 — fully fixed: v2 now requires positive confirmation,
208
+ `transport.constructor.name === 'StdioServerTransport'`, instead of
209
+ inferring single-connection from an absent `sessionId` property), and
210
+ Agent Thrash Detection's fallback session id is now registry-backed via
211
+ `instanceKey` (entry 6's fallback-id gap — fixed: repeated
212
+ `instrumentMcpServer()` calls sharing an `instanceKey` now reuse the same
213
+ generated id instead of a fresh one per call). Both fixes shipped in the
214
+ same commit, in a specific order — fixing detection (entry 8) before
215
+ sharing the fallback id (entry 6) — since sharing it first would have made
216
+ entry 8's false positive worse, not better.
217
+
218
+ **What remains genuinely open, narrower than either original gap:**
219
+ `thrashSessionState` (whether a server has ever proven itself
220
+ session-aware) still isn't registry-backed, and — structurally, not a
221
+ bug this library can fix — MCP spec 2026-07-28 removes protocol-level
222
+ sessions entirely, so no configuration of this library can produce a
223
+ *real* session id for a spec-2026-07-28-native deployment in the first
224
+ place. See ADR 015's final "Update ... Findings 3 and 8 landed here too"
225
+ section and `docs/known-gaps.md` entries 6 and 8 for the full accounting.
226
+
3
227
  ## 0.9.0
4
228
 
5
229
  **⚠️ Fixed, with a fingerprint behavior change — read this before the