mcp(telemetry): put the tool call's outcome on the OTel span instead of only in the log line - #10230
Conversation
…of only in the log line Fixes JSONbored#10042
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-31 13:31:35 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10230 +/- ##
==========================================
+ Coverage 80.47% 80.50% +0.02%
==========================================
Files 282 285 +3
Lines 58856 58925 +69
Branches 6978 6993 +15
==========================================
+ Hits 47366 47435 +69
Misses 11199 11199
Partials 291 291
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Summary
packages/loopover-contract/src/telemetry.ts:248declares the span contract:src/mcp/dispatch-telemetry.ts:15repeats the promise in the chokepoint's own header: "an OTel spanmcp.tool/<name>on the self-host path, whose attributes are a strict subset -- never arguments."buildMcpToolSpanAttributesnever reaches a span. Its only two call sites are the two structured loglines —
src/mcp/dispatch-telemetry.ts:116and:135:The span's actual attributes are a separate literal built before the call runs, at
src/mcp/dispatch-telemetry.ts:84:So a self-hosted operator's tracing backend receives
mcp.tool/<name>spans carryingtool,categoryand
surfaceand nothing else.ok,transportand — the one that matters for triage —error_codenever reach the span, so a trace view cannot be filtered or grouped by cause the way the PostHog view
can.
withOtelSpan(src/selfhost/otel.ts:348) setsSpanStatusCode.ERRORon a throw, which is the onlyoutcome signal the span carries today; the resolved closed-set code that the very same
callobjectalready holds is dropped.
The gap is structural, not an oversight at one line:
DispatchTelemetrySink.withSpan(
src/mcp/dispatch-telemetry.ts:51) takes its attributes once, at open, and exposes no way to add anybefore the span ends — so
instrumentToolDispatchhas no seam through which to publish an outcome it onlylearns after the handler returns.
Deliverables
DispatchTelemetrySink.withSpan's signature gains a seam for post-hoc attributes, andNOOP_DISPATCH_SINKpluscreateDispatchTelemetrySink(
src/mcp/dispatch-telemetry-sink.ts:75) both implement it.instrumentToolDispatchpublishesbuildMcpToolSpanAttributes(call)onto the span on the returnpath and on the throw path.
src/selfhost/otel.ts's runner (or the closure the self-host entry registers viasetMcpDispatchSpanRunner) applies those attributes to the real span throughotelSafeAttributes,the same scrubber every other attribute goes through.
test/unit/mcp-dispatch-telemetry.test.tsnamed for this bug that injects arecording
withSpansink, runs a handler that returns normally and one that throws, and asserts thespan for each ends with
okand — for the throw — anerror_codedrawn fromMCP_TELEMETRY_ERROR_CODES.NOOP_DISPATCH_SINK.withSpanis still a pure passthrough that records nothing andreturns the handler's value unchanged.
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example adding the attributes on the success path only, or changing the sink signature without wiring
the self-host runner — does not resolve this issue.
Test plan
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includecoverssrc/**/*.ts(line 78) andpackages/loopover-contract/src/**/*.ts(line 108),so every touched path is measured and gated. Both arms of each branch need a test: the return path
versus the throw path in
instrumentToolDispatch, the...(call.errorCode ? { error_code } : {})spreadin
buildMcpToolSpanAttributes(already covered bytest/unit/mcp-dispatch-telemetry.test.ts:71and:75— keep both), thecall.transport ?? "local"nullish arm, and thewithSpan ?? getMcpDispatchSpanRunner() ?? passthroughchain atsrc/mcp/dispatch-telemetry-sink.ts:94, whose three arms must each be exercised.Fixes #10042