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 +224 -0
- package/README.md +503 -55
- package/package.json +12 -1
- package/src/attributes.js +45 -0
- package/src/config.js +78 -6
- package/src/cost/calculator.d.ts +14 -2
- package/src/cost/calculator.js +48 -17
- package/src/cost/pricing.d.ts +18 -1
- package/src/cost/pricing.js +91 -26
- package/src/cost/types.d.ts +42 -5
- package/src/fingerprint/classify/channel.js +83 -7
- package/src/fingerprint/classify/validation-paths.js +134 -25
- package/src/index.d.ts +125 -20
- package/src/index.js +1 -1
- package/src/instrument.js +490 -101
- package/src/metrics.js +14 -4
- package/src/sdk/detect.js +118 -0
- package/src/tracecontext/extract.d.ts +10 -0
- package/src/tracecontext/extract.js +128 -0
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
|