opentel-mcp 0.6.1 → 0.8.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 +346 -0
- package/README.md +830 -13
- package/package.json +3 -2
- package/src/attributes.js +3 -0
- package/src/config.js +12 -0
- package/src/cost/budget.js +13 -4
- package/src/fingerprint/attributes.d.ts +1 -1
- package/src/fingerprint/attributes.js +38 -1
- package/src/fingerprint/classify/channel.d.ts +9 -0
- package/src/fingerprint/classify/channel.js +223 -0
- package/src/fingerprint/classify/validation-paths.js +166 -0
- package/src/fingerprint/types.d.ts +40 -0
- package/src/index.d.ts +68 -0
- package/src/instrument.js +332 -16
- package/src/observation/integrity.js +108 -0
- package/src/observation/tool-outcome-counter.js +131 -0
- package/src/observation/types.d.ts +78 -0
- package/src/schema-drift/attributes.js +68 -0
- package/src/schema-drift/canonicalize.js +80 -0
- package/src/schema-drift/compose.js +94 -0
- package/src/schema-drift/config.js +111 -0
- package/src/schema-drift/detector.js +135 -0
- package/src/schema-drift/diff.js +176 -0
- package/src/schema-drift/emitter.js +106 -0
- package/src/schema-drift/store.js +75 -0
- package/src/schema-drift/types.d.ts +134 -0
- package/src/thrash/attributes.js +38 -0
- package/src/thrash/config.js +17 -0
- package/src/thrash/detector.js +96 -14
- package/src/thrash/emitter.js +15 -6
- package/src/thrash/types.d.ts +35 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,351 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
Three features. **Tool schema drift detection**: a server that silently
|
|
6
|
+
changes a tool's `inputSchema` between deployments — a parameter renamed, a
|
|
7
|
+
type tightened, a `required` field added — currently breaks agents with no
|
|
8
|
+
signal pointing at the actual cause. Full investigation and design: ADR 010
|
|
9
|
+
(`docs/adr/010-schema-drift.md`). **Two-axis observation contract**:
|
|
10
|
+
prompted by external review (Massimiliano Brighindi), who first raised that
|
|
11
|
+
`instrumentMcpServer()` with no `TracerProvider`/`MeterProvider` registered
|
|
12
|
+
silently no-ops — a failed tool call in that state produces zero telemetry,
|
|
13
|
+
indistinguishable from one that never failed — and then supplied the reframe
|
|
14
|
+
that shaped what shipped: not "detect a broken pipeline," but "stop implying
|
|
15
|
+
health by omission." Full investigation and design: ADR 008
|
|
16
|
+
(`docs/adr/008-observation-liveness.md`, "Update (2026-08-05)" section).
|
|
17
|
+
**Cost-aware trace sampling**: investigated whether traces that were
|
|
18
|
+
expensive or thrashed could be kept regardless of the head sampler's
|
|
19
|
+
decision. Found that this cannot be an in-process library feature — a
|
|
20
|
+
`Sampler` decides at span start, cost/thrash are only known at span end, and
|
|
21
|
+
this package doesn't own the `Sampler`/`SpanProcessor` chain in its default
|
|
22
|
+
configuration anyway. Ships a marker attribute plus a documented Collector
|
|
23
|
+
recipe instead of a sampler. Full investigation and design: ADR 011
|
|
24
|
+
(`docs/adr/011-cost-aware-sampling.md`).
|
|
25
|
+
|
|
26
|
+
**⚠️ Read "Changed — behavior change on upgrade" immediately below before
|
|
27
|
+
updating.** One of the three features above changes when
|
|
28
|
+
`instrumentMcpServer()` throws, for a subset of low-level `Server` users,
|
|
29
|
+
purely from the new default-on config — no code change of your own
|
|
30
|
+
required to hit it.
|
|
31
|
+
|
|
32
|
+
### Changed — behavior change on upgrade, read before updating
|
|
33
|
+
|
|
34
|
+
- **The instrument-first ordering requirement now also covers `tools/list`,
|
|
35
|
+
for low-level `Server` users specifically, because `schemaDrift.enabled`
|
|
36
|
+
defaults to `true`.** `instrumentMcpServer()` has always required being
|
|
37
|
+
called before any `tools/call` handler is registered; as of this release,
|
|
38
|
+
with schema drift enabled (the default), it also requires being called
|
|
39
|
+
before any `tools/list` handler is registered. If your low-level `Server`
|
|
40
|
+
code registers `server.setRequestHandler(ListToolsRequestSchema, ...)`
|
|
41
|
+
before calling `instrumentMcpServer()` — never previously an error, since
|
|
42
|
+
this library was blind to `tools/list` entirely before this release —
|
|
43
|
+
upgrading will make that throw `INSTRUMENT_FIRST_ERROR` where it didn't
|
|
44
|
+
before, with no other code changes on your part.
|
|
45
|
+
- **`McpServer` users are unaffected.** `McpServer` registers `tools/list`
|
|
46
|
+
and `tools/call` together, atomically, the first time `.tool()` or
|
|
47
|
+
`.registerTool()` is called — so anyone already following the
|
|
48
|
+
documented instrument-before-registration rule for `tools/call`
|
|
49
|
+
automatically satisfies it for `tools/list` too.
|
|
50
|
+
- **Migration**: either reorder your `tools/list` registration to after
|
|
51
|
+
`instrumentMcpServer()`, or pass `schemaDrift: { enabled: false }` to
|
|
52
|
+
opt out and keep your existing registration order — both fully
|
|
53
|
+
restore v0.7.0 behavior. There is no change if you don't use a
|
|
54
|
+
low-level `Server` with an independently-registered `tools/list`
|
|
55
|
+
handler.
|
|
56
|
+
- Only `schemaDrift` (schema drift detection, below) causes this — the
|
|
57
|
+
two-axis observation contract and cost-aware sampling features in this
|
|
58
|
+
same release are purely additive, with no effect on when
|
|
59
|
+
`instrumentMcpServer()` throws.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **Tool schema drift detection.** Every `tools/list` response is captured,
|
|
64
|
+
canonicalized, and hashed per tool (`inputSchema` only — `description` is
|
|
65
|
+
a deliberately separate, not-yet-built dimension; see ADR 010). When a
|
|
66
|
+
previously-observed tool's schema hash changes, this emits:
|
|
67
|
+
- An `mcp.tool.schema_drift.detected` counter, labeled `gen_ai.tool.name`
|
|
68
|
+
and `mcp.tool.schema_drift.type` (both bounded — see
|
|
69
|
+
`METRIC_SAFE_ATTRIBUTES`, `src/schema-drift/attributes.js`).
|
|
70
|
+
- An `mcp.tool.schema_drift.detected` span event on the new `tools/list`
|
|
71
|
+
span (see Changed, below), carrying the drift `type`, the previous/
|
|
72
|
+
current schema hash, and — only when non-empty, never set to `[]` —
|
|
73
|
+
which field names were added/removed/changed. Field names are
|
|
74
|
+
span-only, never a metric label (unbounded across tools/deployments,
|
|
75
|
+
same reasoning as `mcp.failure.validation_paths`, ADR 009).
|
|
76
|
+
- `type` is one of `field_added` \| `field_removed` \| `type_changed` \|
|
|
77
|
+
`required_changed` \| `multiple` (more than one at once, never guessed
|
|
78
|
+
down to a single answer) \| `unknown` (a change this differ can't
|
|
79
|
+
confidently characterize — e.g. a top-level `oneOf`/`anyOf`/`allOf`
|
|
80
|
+
composition, or a change hidden behind a `$ref`/`$defs` indirection
|
|
81
|
+
that doesn't touch the referencing property's own value — still
|
|
82
|
+
reported as drift, just not attributable to a specific field).
|
|
83
|
+
- The **first** observation of any given tool is never reported as
|
|
84
|
+
drift (nothing to compare against yet — cold start). A tool that
|
|
85
|
+
stops appearing in `tools/list` responses for a while and later
|
|
86
|
+
reappears is compared against its last-seen schema, not treated as a
|
|
87
|
+
fresh cold start — see the README's "Tool schema drift detection"
|
|
88
|
+
section for why this is the correct, and possibly counter-intuitive,
|
|
89
|
+
behavior.
|
|
90
|
+
- State is scoped **per instrumented server instance, not per session**
|
|
91
|
+
(ADR 010, Q4) — every client session sees the same tool registry, so
|
|
92
|
+
session-keyed state would produce false cold-starts per new session
|
|
93
|
+
and could silently swallow drift that happened between sessions.
|
|
94
|
+
- New `schemaDrift` option on `instrumentMcpServer()`, following the
|
|
95
|
+
exact `thrashDetection`/`costTracking` partial-overrides-individual-
|
|
96
|
+
defaults pattern: `enabled` (default `true`) and `maxTrackedTools`
|
|
97
|
+
(default `1000`, an LRU cap — defense-in-depth, not a response to an
|
|
98
|
+
expected failure mode; a server's own tool count is normally small).
|
|
99
|
+
Each independently overridable via `OTEL_MCP_SCHEMA_DRIFT_ENABLED` /
|
|
100
|
+
`OTEL_MCP_SCHEMA_DRIFT_MAX_TRACKED_TOOLS`, following the existing
|
|
101
|
+
`OTEL_MCP_THRASH_*` env-var convention.
|
|
102
|
+
**Unlike** `thrashDetection`/`costTracking`, `schemaDrift.enabled: false`
|
|
103
|
+
is a true no-op: `tools/list` is not wrapped at all (no span, no
|
|
104
|
+
capture, no detector/emitter construction) — the `tools/list` span
|
|
105
|
+
this feature introduces exists purely for schema drift, unlike the
|
|
106
|
+
`tools/call` span, which already serves other purposes regardless of
|
|
107
|
+
sub-feature flags.
|
|
108
|
+
- `SchemaDriftConfig`, `SchemaDriftKind`, `SchemaDriftEvent` types,
|
|
109
|
+
exported from the package root.
|
|
110
|
+
|
|
111
|
+
### Added — Two-axis observation contract
|
|
112
|
+
|
|
113
|
+
- **`getObservationState()`**, a new accessor attached to the object
|
|
114
|
+
`instrumentMcpServer()` returns — unconditional (not gated behind
|
|
115
|
+
`setupNodeSdk`), same additive pattern as `shutdown()`/`getThrashSummary()`,
|
|
116
|
+
omitted entirely when `options.enabled` is `false`. Returns:
|
|
117
|
+
```
|
|
118
|
+
{
|
|
119
|
+
toolOutcome: { success, failure, unknown },
|
|
120
|
+
observationIntegrity: 'DEGRADED' | 'UNKNOWN',
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
No OTel emission — purely in-process, nothing sent anywhere, safe to
|
|
124
|
+
call from application code (a health-check endpoint, a periodic
|
|
125
|
+
`console.log`, a debugger). See the README's "Two-axis observation
|
|
126
|
+
contract" section for the full design rationale.
|
|
127
|
+
- **`toolOutcome`**: cumulative tool-call outcome counts since
|
|
128
|
+
instrumentation, from a **new counter that increments on every tool
|
|
129
|
+
call unconditionally** — independent of `fingerprinting`,
|
|
130
|
+
`thrashDetection`, and `enableMetrics`. Deliberately NOT read off
|
|
131
|
+
`ThrashDetector`/`getThrashSummary()`: that bookkeeping only runs
|
|
132
|
+
when a fingerprint was computed, so with `fingerprinting: false` (a
|
|
133
|
+
fully supported configuration) it would silently report zero
|
|
134
|
+
failures regardless of how many actually occurred — the exact
|
|
135
|
+
silent-success failure mode this feature exists to close. A
|
|
136
|
+
malformed, unrecognizable tool result (not a real `CallToolResult`
|
|
137
|
+
shape) increments `unknown` rather than silently defaulting to
|
|
138
|
+
`success`.
|
|
139
|
+
- **`observationIntegrity`**: `'DEGRADED' | 'UNKNOWN'` — note there is
|
|
140
|
+
no `'HEALTHY'` value, and this is not an oversight. Investigated and
|
|
141
|
+
found structurally unreachable in every configuration: the one lead
|
|
142
|
+
(OTel SDK self-observability metrics) is a write-only `Counter` with
|
|
143
|
+
no synchronous read-back API in `@opentelemetry/api`, so this
|
|
144
|
+
library's own code can never positively confirm telemetry is
|
|
145
|
+
flowing, no matter how it's wired up. `HEALTHY` is therefore absent
|
|
146
|
+
from the type entirely, not merely never returned — enforced by
|
|
147
|
+
TypeScript, not just documentation (see the new type-level tests in
|
|
148
|
+
`test/index.exports.test-d.ts`).
|
|
149
|
+
- `DEGRADED` is detected via a fragile `ProxyTracerProvider`
|
|
150
|
+
reference-equality check, and is reachable **only** under
|
|
151
|
+
`setupNodeSdk: false` (the default) — when no `TracerProvider` has
|
|
152
|
+
been registered globally at all.
|
|
153
|
+
- Under `setupNodeSdk: true`, this library registers the provider
|
|
154
|
+
itself, so absence can never be confirmed — `observationIntegrity`
|
|
155
|
+
is **always** `'UNKNOWN'` in that configuration, without even
|
|
156
|
+
attempting the check.
|
|
157
|
+
- Recomputed fresh on **every call** to `getObservationState()`,
|
|
158
|
+
never cached from instrument time — a host may register a
|
|
159
|
+
`TracerProvider` asynchronously after `instrumentMcpServer()`
|
|
160
|
+
already ran, and a value cached at startup would go stale the
|
|
161
|
+
moment that happens.
|
|
162
|
+
- `ToolOutcome`, `ToolOutcomeCounts`, `ObservationIntegrity`,
|
|
163
|
+
`ObservationState` types, exported from the package root.
|
|
164
|
+
|
|
165
|
+
### Added — Cost-aware trace sampling (marker attribute + Collector recipe)
|
|
166
|
+
|
|
167
|
+
- **`mcp.tool.thrash_detected`, a new boolean span attribute**, set
|
|
168
|
+
alongside (never instead of) the existing `mcp.loop.detected` span
|
|
169
|
+
event, in the same `thrash/emitter.js` call site — set only when
|
|
170
|
+
`thrashDetection` is enabled and a loop was actually detected on this
|
|
171
|
+
call, same reachability as the existing event, no new failure mode.
|
|
172
|
+
Exists specifically so an OpenTelemetry Collector's
|
|
173
|
+
`tailsamplingprocessor` has an unambiguous, attribute-level signal to
|
|
174
|
+
key on: whether a `boolean_attribute` policy can also match span-*event*
|
|
175
|
+
data was investigated and left genuinely unverified (the processor is
|
|
176
|
+
Go source in a separate repository, not installed here), so this
|
|
177
|
+
attribute removes that uncertainty entirely rather than leaving tail
|
|
178
|
+
sampling dependent on an unconfirmed answer. **Named deliberately
|
|
179
|
+
differently** from the pre-existing `mcp.tool.loop.detected` **metric**
|
|
180
|
+
counter, not reusing its string as ADR 011 originally specified — see
|
|
181
|
+
that ADR's "Update" note. A metric name and a span attribute key are
|
|
182
|
+
unrelated OTel namespaces with no technical conflict, but reusing the
|
|
183
|
+
name left the one reader who most needs it to be unambiguous — someone
|
|
184
|
+
writing a Collector tail-sampling policy — unable to tell, from the
|
|
185
|
+
name alone, which of the two same-named signals they were keying on.
|
|
186
|
+
- **No new cost-threshold attribute or config.** `mcp.tool.cost.usd` and
|
|
187
|
+
`mcp.tool.cost.budget_exceeded` (both already shipped, v0.5.0) already
|
|
188
|
+
fully suffice for a Collector `numeric_attribute` / `boolean_attribute`
|
|
189
|
+
policy — the numeric threshold itself lives entirely in the
|
|
190
|
+
Collector's own policy config (the YAML), not in this package's
|
|
191
|
+
`InstrumentOptions`. No new env var, no new `instrumentMcpServer()`
|
|
192
|
+
option.
|
|
193
|
+
- **A documented, pasteable OpenTelemetry Collector `tailsamplingprocessor`
|
|
194
|
+
config** (README's "Cost-aware trace sampling" section) keeping any
|
|
195
|
+
trace with an expensive call, a budget-exceeded call, or a detected
|
|
196
|
+
thrash loop, alongside an ordinary probabilistic sample for everything
|
|
197
|
+
else.
|
|
198
|
+
- **No in-process sampler or buffering `SpanProcessor` was built, and none
|
|
199
|
+
is planned** — investigated and rejected on two independent grounds
|
|
200
|
+
(ADR 011): this package doesn't own the `Sampler`/`SpanProcessor` chain
|
|
201
|
+
in its default configuration (no public API to inject either into a
|
|
202
|
+
host-owned `TracerProvider`), and even where a custom processor could
|
|
203
|
+
theoretically be installed, an in-process decision can only ever rescue
|
|
204
|
+
the one span this package itself creates — never an already-finished
|
|
205
|
+
child span from other instrumentation, never an upstream span in a
|
|
206
|
+
different process. "Keep the trace" is not achievable in-process; at
|
|
207
|
+
best, "keep this one span" is, which is a materially smaller guarantee
|
|
208
|
+
than the stated goal. Real cross-span, cross-process trace buffering is
|
|
209
|
+
what the Collector's `tailsamplingprocessor` already does correctly —
|
|
210
|
+
not something to partially re-implement inside this package.
|
|
211
|
+
|
|
212
|
+
## 0.7.0
|
|
213
|
+
|
|
214
|
+
Origin-aware failure classification for Agent Thrash Detection. Prompted by
|
|
215
|
+
external review (Reddit) pointing out that failure detection only read the
|
|
216
|
+
`isError` channel, missing JSON-RPC protocol-level failures. Verifying that
|
|
217
|
+
report surfaced a separate, more consequential finding: a real false
|
|
218
|
+
positive already live in every published version — output-validation
|
|
219
|
+
failures (a server-side bug) being counted as agent thrash. Full
|
|
220
|
+
investigation and design: ADR 007 (`docs/adr/007-protocol-error-channel.md`).
|
|
221
|
+
|
|
222
|
+
Also investigates a second external report (u/Pleasant-Ad192): whether a
|
|
223
|
+
different schema field failing validation each attempt (an agent
|
|
224
|
+
converging) can be told apart from the same field failing repeatedly (an
|
|
225
|
+
ambiguous tool schema). Finding: it mostly already can be, as a side
|
|
226
|
+
effect of how failures are fingerprinted — see ADR 009
|
|
227
|
+
(`docs/adr/009-field-level-convergence.md`) — now pinned down by
|
|
228
|
+
regression tests and a diagnostic span attribute, with one real gap
|
|
229
|
+
(partial convergence) still open pending a design decision.
|
|
230
|
+
|
|
231
|
+
Known gaps and open questions this release didn't close: `docs/known-gaps.md`.
|
|
232
|
+
|
|
233
|
+
### Fixed
|
|
234
|
+
|
|
235
|
+
- **Output-validation failures were miscategorized and incorrectly counted
|
|
236
|
+
toward Agent Thrash Detection.** When a tool's own handler returned output
|
|
237
|
+
that didn't match its declared output schema, the resulting failure
|
|
238
|
+
(surfaced as `isError: true`, whether thrown directly or converted by the
|
|
239
|
+
high-level `McpServer`) landed in fingerprint category `validation` or
|
|
240
|
+
`internal` depending on the exact wording, and — since Agent Thrash
|
|
241
|
+
Detection shipped in v0.6.0 — was tracked exactly like a normal
|
|
242
|
+
business-logic failure. An agent retrying such a tool would eventually
|
|
243
|
+
cross the default threshold and get flagged as "thrashing," even though
|
|
244
|
+
the failure is entirely the tool author's bug: no argument the agent
|
|
245
|
+
supplies can ever fix a server that never returns valid structured
|
|
246
|
+
content. **Affected range: the miscategorization itself has been present
|
|
247
|
+
since v0.4.0 (deep-failure fingerprinting); the false-positive thrash
|
|
248
|
+
count has been present since v0.6.0 (Agent Thrash Detection), through the
|
|
249
|
+
last published release, v0.6.1.** Fixed by classifying which *channel* a
|
|
250
|
+
failure arrived on (see Added, below) and excluding the `protocol.output`
|
|
251
|
+
channel from thrash detection entirely — not merely relabeling it.
|
|
252
|
+
|
|
253
|
+
### Added
|
|
254
|
+
|
|
255
|
+
- **`mcp.failure.channel` span attribute** — one of `execution` |
|
|
256
|
+
`protocol.not_found` | `protocol.input` | `protocol.output` |
|
|
257
|
+
`protocol.other` | `unknown`, classifying which channel a tools/call
|
|
258
|
+
failure arrived on (`classifyFailureChannel()`,
|
|
259
|
+
`src/fingerprint/classify/channel.js`). Additive: never part of
|
|
260
|
+
`computeFingerprint()`'s hash input (see Unchanged, below) — deliberately
|
|
261
|
+
a separate attribute from the pre-existing `mcp.failure.origin`, which
|
|
262
|
+
means something different (`tool_error` \| `thrown` \| `transport`) and
|
|
263
|
+
has been hashed since v0.4.0.
|
|
264
|
+
- **Per-channel Agent Thrash Detection thresholds**: `inputThreshold`
|
|
265
|
+
(default `5`, higher than the base `threshold`) for the `protocol.input`
|
|
266
|
+
channel — an agent retrying with different arguments after an
|
|
267
|
+
input-validation failure may be genuinely converging, not thrashing —
|
|
268
|
+
and `notFoundThreshold` (default `1`, an immediate flag) for
|
|
269
|
+
`protocol.not_found` — retrying a tool name that doesn't exist is never
|
|
270
|
+
convergence. Each independently overridable via its own env var
|
|
271
|
+
(`OTEL_MCP_THRASH_INPUT_THRESHOLD` / `OTEL_MCP_THRASH_NOT_FOUND_THRESHOLD`),
|
|
272
|
+
following the exact existing `OTEL_MCP_THRASH_*` pattern.
|
|
273
|
+
- `FailureChannel` type, exported from the package root alongside the
|
|
274
|
+
existing `FailureCategory` / `FailureOrigin` types.
|
|
275
|
+
- For high-level `McpServer` users specifically: since `McpServer` converts
|
|
276
|
+
most protocol-shaped failures (tool not found, disabled, input/output
|
|
277
|
+
validation) to `isError: true` before this library ever sees a thrown
|
|
278
|
+
error, `classifyFailureChannel()` also recovers the real channel from
|
|
279
|
+
that disguised form by reading the `MCP error {code}: ` wrapper
|
|
280
|
+
`McpError`'s constructor always applies, which `McpServer` preserves
|
|
281
|
+
verbatim. Without this, `protocol.output`'s exclusion (the fix above)
|
|
282
|
+
would only have applied to hand-rolled low-level `Server` apps, not to
|
|
283
|
+
`McpServer` — see the README's "Agent Thrash Detection" section and ADR
|
|
284
|
+
007's addendum for the full reachability picture and its limits.
|
|
285
|
+
- **`mcp.failure.validation_paths` span attribute** — which schema
|
|
286
|
+
field(s) a Zod validation failure named, one dot-joined path per
|
|
287
|
+
failing issue (e.g. `["email", "user.profile.age"]`), best-effort
|
|
288
|
+
extracted from the same message text `classifyFailureChannel()` already
|
|
289
|
+
reads (`extractValidationPaths()`,
|
|
290
|
+
`src/fingerprint/classify/validation-paths.js`). Omitted entirely —
|
|
291
|
+
never set to an empty array — when nothing confidently parseable was
|
|
292
|
+
found. Span-only, permanently excluded from
|
|
293
|
+
`METRIC_SAFE_ATTRIBUTES`: field/path names are bounded per tool but
|
|
294
|
+
unbounded across every tool anyone registers, the same reasoning that
|
|
295
|
+
already keeps `mcp.failure.fingerprint`/`signature`/`error_class` off
|
|
296
|
+
metric labels. Full investigation and design: ADR 009
|
|
297
|
+
(`docs/adr/009-field-level-convergence.md`).
|
|
298
|
+
|
|
299
|
+
### Unchanged
|
|
300
|
+
|
|
301
|
+
- **Fingerprints (`mcp.failure.fingerprint` and every other
|
|
302
|
+
`FingerprintInputs` field) are byte-identical to v0.6.1 for the same
|
|
303
|
+
inputs.** The new `channel` dimension is deliberately kept out of
|
|
304
|
+
`computeFingerprint()`'s hash input (ADR 007) specifically so this
|
|
305
|
+
release cannot change any consumer's existing `mcp.failure.fingerprint`
|
|
306
|
+
values — a change there would silently break any alert or dashboard
|
|
307
|
+
built on fingerprint identity. Verified, not just asserted: by extracting
|
|
308
|
+
the actual, published `v0.6.1` git tag's `src/fingerprint/` tree via `git
|
|
309
|
+
archive` into an isolated directory and running its `computeFingerprint()`
|
|
310
|
+
directly, independent of this working tree, against six fixture inputs —
|
|
311
|
+
see `test/fingerprint/compose.fixtures.test.js`. If you have alerts or
|
|
312
|
+
dashboards keyed on `mcp.failure.fingerprint`, they keep working exactly
|
|
313
|
+
as they did on v0.6.1, with no changes required on your end.
|
|
314
|
+
- **Field-level discrimination in Agent Thrash Detection is not a new
|
|
315
|
+
capability — it already worked, as a side effect of fingerprinting the
|
|
316
|
+
full Zod issues JSON, and was simply incidental until now.** A
|
|
317
|
+
validation failure repeating on the *same* schema field across attempts
|
|
318
|
+
already hashed to the *same* fingerprint (accumulating correctly toward
|
|
319
|
+
`inputThreshold`), and a *different* field failing each attempt already
|
|
320
|
+
hashed to a *different* fingerprint each time (never accumulating,
|
|
321
|
+
matching a converging agent). Investigated and confirmed in ADR 009; now
|
|
322
|
+
pinned down by regression tests
|
|
323
|
+
(`test/fingerprint/field-level-convergence.test.js`) so a future Zod or
|
|
324
|
+
SDK change that silently breaks it gets caught, rather than discovered
|
|
325
|
+
as a production regression. One real gap remains open and is *not*
|
|
326
|
+
fixed by this: partial convergence (fixing one of several failing
|
|
327
|
+
fields changes the issues array's shape and breaks fingerprint
|
|
328
|
+
continuity) — tracked in `docs/known-gaps.md`, pending a design ADR 009
|
|
329
|
+
did not settle on.
|
|
330
|
+
|
|
331
|
+
### Documentation
|
|
332
|
+
|
|
333
|
+
- `docs/known-gaps.md` (new): five tracked gaps this release didn't close,
|
|
334
|
+
each written as a ready-to-paste GitHub issue — field-level convergence
|
|
335
|
+
tracking, partial convergence in field-level validation, the
|
|
336
|
+
observation-liveness contract, the pre-handler parse-failure gap, and
|
|
337
|
+
how client-side retry caps interact with
|
|
338
|
+
detection thresholds.
|
|
339
|
+
- README's "Agent Thrash Detection" section now covers channel-aware
|
|
340
|
+
thresholds, the `McpServer`-vs-low-level-`Server` reachability
|
|
341
|
+
difference (with ADR 007's full table), the pre-handler parse-failure
|
|
342
|
+
gap (deferred, not solved — closing it means revisiting ADR 001), and
|
|
343
|
+
the forwarded-error collision risk as a named known limitation.
|
|
344
|
+
- README's "Failure Fingerprinting" section now documents
|
|
345
|
+
`mcp.failure.validation_paths` and states plainly that field-level
|
|
346
|
+
discrimination is a property of the fingerprint, not a separate
|
|
347
|
+
detector — see ADR 009.
|
|
348
|
+
|
|
3
349
|
## 0.6.1
|
|
4
350
|
|
|
5
351
|
### Fixed
|