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
MCP integration records tool calls that return isError as successful #7890
Call an MCPServer (mcp 2.3.0) tool that raises ValueError, with MCPIntegration enabled.
Expected Result
The client gets isError: true and the tools/call span is marked as an error.
Actual Result
The client gets isError: true, but the span status stays ok, there is no error.type, and the MCP integration captures no error, so MCP tool error rates read 0%.
The mcp 2.x middleware never reads result["isError"] (mcp.py#L457-L501), and the mcp 1.x wrapper ignores a returned CallToolResult(isError=True) (mcp.py#L357-L395). Affected: raised errors on mcp 2.3.0 and fastmcp 4.0.11, and returned isError results on mcp 1.30.0.
I'd like to open a PR to fix this, with tests, if that's okay with you.
Some numbers in case they help: with 25% of MCP tool calls returning isError, Sentry's span data shows a 0% failure rate for the Python server and 25% for the JS one. https://4ktluffy.github.io/mcp-incident-lab/
The JS integration already handles this (correlation.js). Happy to port it to Python with tests if you're open to a PR.
For graphql we don't set the span status to error when an error is reported because the response code is 200.
This seems like a similar situation, so it's unclear that we want the span status to be error.
The JS SDK already treats it as an error: for a tools/call result with isError=true it sets the span status to internal_error and error.type to "tool_error" (correlation.js). So today the same failing tool shows 25% failures from a JS MCP server and 0% from a Python one.
Even if the status should stay ok, SPANDATA.MCP_TOOL_RESULT_IS_ERROR is already defined in consts.py but never set. Setting it would let people count and alert on failed tool calls without changing the span status.
Would a PR that only sets mcp.tool.result.is_error be useful as a first step? Happy to do it with tests, or to match whatever the JS side ends up doing.
On the tool output: it is only recorded when prompts/outputs are allowed (send_default_pii or data_collectiongen_ai.outputs), so with the defaults it is not on the span at all. And when it is, it is free text like "warehouse DB timeout", so it can't be counted or alerted on as a failure rate.
Thanks! If it helps, I can open a PR that sets error.type="tool_error" when a tool call returns isError=true, with tests for both mcp 1.x and 2.x, and leaves the span status as it is. Just let me know.
The mcp 1.x handler wrapper and the mcp 2.x tool-call middleware only record a failure when the handler raises and that exception is caught. A normal return of a CallToolResult, or of a dict whose isError field is true, skips that path, so the span is finished without error.type.
After the handler returns, both paths now use a helper that treats isError as true on either a dict or a result object.
Activity
linear-code commentedon Oct 6, 2026
PY-2934
alexander-alderman-webb commentedon Oct 6, 2026
Thanks for reporting!
We'll be tackling these gaps in the coming months.
4ktLuffy commentedon Oct 7, 2026
Some numbers in case they help: with 25% of MCP tool calls returning
isError, Sentry's span data shows a 0% failure rate for the Python server and 25% for the JS one. https://4ktluffy.github.io/mcp-incident-lab/The JS integration already handles this (
correlation.js). Happy to port it to Python with tests if you're open to a PR.alexander-alderman-webb commentedon Oct 13, 2026
For graphql we don't set the span status to error when an error is reported because the response code is 200.
This seems like a similar situation, so it's unclear that we want the span status to be error.
4ktLuffy commentedon Oct 13, 2026
Thanks Alex, that makes sense for GraphQL.
Two things that might make MCP different:
tools/callresult withisError=trueit sets the span status tointernal_erroranderror.typeto"tool_error"(correlation.js). So today the same failing tool shows 25% failures from a JS MCP server and 0% from a Python one.SPANDATA.MCP_TOOL_RESULT_IS_ERRORis already defined inconsts.pybut never set. Setting it would let people count and alert on failed tool calls without changing the span status.Would a PR that only sets
mcp.tool.result.is_errorbe useful as a first step? Happy to do it with tests, or to match whatever the JS side ends up doing.alexander-alderman-webb commentedon Oct 13, 2026
mcp.tool.result.is_erroris not in OTel so let's not set it.Isn't this information already contained in the tool output content?
4ktLuffy commentedon Oct 13, 2026
Fair point on the custom attribute, I'll drop that idea.
OTel does cover this case though, through
error.typerather than a new attribute. The MCP conventions say that when the JSON-RPC call succeeds but the result carries an error,error.typeSHOULD be set, and for aCallToolResultwithisError=trueit SHOULD be"tool_error":https://github.com/open-telemetry/semantic-conventions-genai/blob/4a39b6ef1363fab57bc40e18c82abb32cc89ebcd/docs/gen-ai/mcp.md#L187-L192
That is also what the JS SDK does today.
On the tool output: it is only recorded when prompts/outputs are allowed (
send_default_piiordata_collectiongen_ai.outputs), so with the defaults it is not on the span at all. And when it is, it is free text like "warehouse DB timeout", so it can't be counted or alerted on as a failure rate.alexander-alderman-webb commentedon Oct 13, 2026
error.typewould be the OTel compliant way of doing things, so that's an argument in favor.4ktLuffy commentedon Oct 13, 2026
Thanks! If it helps, I can open a PR that sets
error.type="tool_error"when a tool call returnsisError=true, with tests for both mcp 1.x and 2.x, and leaves the span status as it is. Just let me know.mvanhorn commentedon Oct 13, 2026
The mcp 1.x handler wrapper and the mcp 2.x tool-call middleware only record a failure when the handler raises and that exception is caught. A normal return of a CallToolResult, or of a dict whose isError field is true, skips that path, so the span is finished without error.type.
After the handler returns, both paths now use a helper that treats isError as true on either a dict or a result object.
#8020