Skip to content

Commit bf332a8

Browse files
committed
fix(otel): record client JSON-RPC errors on spans
1 parent 6e30452 commit bf332a8

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

src/mcp/shared/jsonrpc_dispatcher.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
ProgressToken,
3333
RequestId,
3434
)
35-
from opentelemetry.trace import SpanKind
35+
from opentelemetry.trace import SpanKind, StatusCode
3636
from pydantic import ValidationError
3737
from typing_extensions import TypeVar
3838

@@ -385,7 +385,7 @@ async def send_raw_request(
385385
span_name,
386386
kind=SpanKind.CLIENT,
387387
attributes={"mcp.method.name": method, "jsonrpc.request.id": str(request_id)},
388-
):
388+
) as span:
389389
# SEP-414: inject W3C trace context; `_meta` stays on the wire even with a no-op tracer.
390390
inject_trace_context(out_meta)
391391
msg = JSONRPCRequest(jsonrpc="2.0", id=request_id, method=method, params=out_params)
@@ -401,6 +401,10 @@ async def send_raw_request(
401401
with anyio.fail_after(opts.get("timeout")):
402402
timeout_armed = True
403403
outcome = await receive.receive()
404+
if isinstance(outcome, ErrorData):
405+
span.set_status(StatusCode.ERROR)
406+
span.set_attribute("error.type", "mcp_error")
407+
span.set_attribute("rpc.response.status_code", outcome.code)
404408
except TimeoutError:
405409
if not timeout_armed:
406410
# `fail_after` arms only after the write, so this TimeoutError is the

tests/server/test_otel.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
from mcp_types import (
1515
INTERNAL_ERROR,
1616
INVALID_PARAMS,
17+
METHOD_NOT_FOUND,
1718
CallToolRequestParams,
1819
CallToolResult,
20+
ErrorData,
1921
GetPromptRequestParams,
2022
GetPromptResult,
2123
ListToolsResult,
@@ -70,6 +72,25 @@ async def test_emits_server_span_with_method_and_target(server: SrvT, spans: Spa
7072
assert span.status.status_code == StatusCode.UNSET
7173

7274

75+
@pytest.mark.anyio
76+
async def test_client_span_records_jsonrpc_error_response(server: SrvT, spans: SpanCapture):
77+
async def missing_method(ctx: Ctx, params: PaginatedRequestParams | None) -> ErrorData:
78+
return ErrorData(code=METHOD_NOT_FOUND, message="missing")
79+
80+
server.add_request_handler("missing", PaginatedRequestParams, missing_method)
81+
async with connected_runner(server) as (client, _):
82+
spans.clear()
83+
with pytest.raises(MCPError) as exc:
84+
await client.send_raw_request("missing", {})
85+
86+
assert exc.value.error.code == METHOD_NOT_FOUND
87+
[span] = [s for s in spans.finished() if s.kind == SpanKind.CLIENT]
88+
assert span.status.status_code == StatusCode.ERROR
89+
assert span.attributes is not None
90+
assert span.attributes["error.type"] == "mcp_error"
91+
assert span.attributes["rpc.response.status_code"] == METHOD_NOT_FOUND
92+
93+
7394
@pytest.mark.anyio
7495
async def test_tool_error_dict_result_sets_error_type(server: SrvT, spans: SpanCapture):
7596
async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:

0 commit comments

Comments
 (0)