You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
OTel client span stays Unset when the tool result has isError: true #3810
With mcp==2.3.0, the client span created in shared/jsonrpc_dispatcher.py sets ERROR status and error.type for a JSON-RPC error response (the #3174 fix), but not for a successful response whose CallToolResult.isError is true. The server span for the same call does set error.type=tool_error (server/_otel.py), so the two ends of one call disagree.
The OTel GenAI semantic conventions for MCP (docs/gen-ai/mcp.md, note on error.type) say instrumentation SHOULD set error.type to tool_error when the tool result reports an error.
Reproduction (Python client and server, Streamable HTTP, default OTel setup):
git clone https://github.com/mkwitko/mcp-reliability && cd mcp-reliability
examples/python-sdk-streamable-http/run.sh # needs docker, uv, go
go run ./cmd/mcp-reliability doctor testdata/python-sdk-default.jsonl
Output includes:
[WARN] MCP-ERR-003: client and server spans for "tools/call fail" disagree on failure: the client span lacks error.type
Suggested change: after the response is parsed in the dispatcher, if the result is a CallToolResult with isError, set error.type="tool_error" and status ERROR on the client span, matching server/_otel.py. Happy to send a PR if you agree with the direction.
Hi! I'd be interested in contributing to this issue.
I went through the reproduction steps and the suggested fix involving setting the client span's error status when a parsed CallToolResult has isError=true.
I'd like to help investigate the dispatcher behavior and add regression coverage to ensure the client and server spans report consistent error statuses.
Is anyone already working on the implementation? If not, would it be okay for me to take this up and submit a PR?
So a response object whose payload reports a tool failure never enters that branch: the outcome decodes fine, the span ends UNSET, and the call looks successful at the client end of the trace.
Server side there is an explicit block for it in src/mcp/server/_otel.py, gated on the method:
Two things in that server block are worth copying rather than paraphrasing, because a client fix that reads naturally will miss them.
The first is the dual match. is_error is dropped by the alias-only wire validation, so a raw dict result arrives as isError. A client change that only tests the model attribute leaves the raw-dict shape disagreeing exactly as it does today. Mirroring the alternation is the whole point.
The second is that this runs after the outcome is parsed and before the value is returned, which is also where the client would have to sit. Setting the attribute on the span after return result is not available to you, and setting it in the caller of the dispatcher means the span has already been closed.
For the test, the interesting assertion is agreement, not the attribute in isolation: same trace, one tool error and one protocol error, assert both spans carry error.type and both are ERROR, and assert a successful call leaves the client span UNSET. That last one is what catches an over-eager fix that marks every tools/call span.
We run an MCP server in front of a few dozen upstream business APIs, and a client-side blind spot on tool errors is expensive in a way that is easy to underestimate: in client dashboards the failed calls present as slow successes, so the retry and timeout logic keys on the wrong signal and the actual error text never surfaces.
Direction is right. If you open the PR, the dual match plus the three-case test is the part I would review first.
Activity
Aryan130103 commentedon Nov 3, 2026
Hi! I'd be interested in contributing to this issue.
I went through the reproduction steps and the suggested fix involving setting the client span's error status when a parsed CallToolResult has isError=true.
I'd like to help investigate the dispatcher behavior and add regression coverage to ensure the client and server spans report consistent error statuses.
Is anyone already working on the implementation? If not, would it be okay for me to take this up and submit a PR?
Thanks!
media-ctrl commentedon Nov 5, 2026
Confirmed on main, and the asymmetry is visible in two specific places rather than being an omission in the dispatcher's design.
Client side,
src/mcp/shared/jsonrpc_dispatcher.pyonly reaches the error attributes inside theisinstance(outcome, ErrorData)branch:So a response object whose payload reports a tool failure never enters that branch: the outcome decodes fine, the span ends UNSET, and the call looks successful at the client end of the trace.
Server side there is an explicit block for it in
src/mcp/server/_otel.py, gated on the method:Two things in that server block are worth copying rather than paraphrasing, because a client fix that reads naturally will miss them.
The first is the dual match.
is_erroris dropped by the alias-only wire validation, so a raw dict result arrives asisError. A client change that only tests the model attribute leaves the raw-dict shape disagreeing exactly as it does today. Mirroring the alternation is the whole point.The second is that this runs after the outcome is parsed and before the value is returned, which is also where the client would have to sit. Setting the attribute on the span after
return resultis not available to you, and setting it in the caller of the dispatcher means the span has already been closed.For the test, the interesting assertion is agreement, not the attribute in isolation: same trace, one tool error and one protocol error, assert both spans carry
error.typeand both are ERROR, and assert a successful call leaves the client span UNSET. That last one is what catches an over-eager fix that marks everytools/callspan.We run an MCP server in front of a few dozen upstream business APIs, and a client-side blind spot on tool errors is expensive in a way that is easy to underestimate: in client dashboards the failed calls present as slow successes, so the retry and timeout logic keys on the wrong signal and the actual error text never surfaces.
Direction is right. If you open the PR, the dual match plus the three-case test is the part I would review first.