Skip to content

Conversation

@Kludex
Copy link
Member

@Kludex Kludex commented Feb 10, 2026

Summary

  • Adds opentelemetry-api as a runtime dependency and opentelemetry-sdk as a dev dependency
  • Creates src/mcp/shared/tracing.py with a module-level tracer, attribute constants, and helpers for span naming (including target extraction for tools/call, prompts/get, resources/read)
  • Instruments BaseSession.send_request() with a single CLIENT span per outgoing request — records method name, target, OK/ERROR status, and error type
  • Excluded methods (e.g. notifications/message) produce no span

This is the minimal foundation for OpenTelemetry support. No context propagation, metrics, or server-side spans yet — just one client-side span per request to establish the pattern.

Test plan

  • Unit tests for _extract_target() and _build_span_name() helpers (parametrized)
  • Integration test: span created with correct name, kind, and OK status on send_ping()
  • Integration test: span name includes tool name for tools/call requests
  • Integration test: ERROR status and error.type attribute on JSON-RPC error response
  • Integration test: no span created for excluded methods
  • ruff format + ruff check pass
  • pyright strict passes (0 errors)
  • Full test suite passes (1157 passed, 0 failed)

Add opentelemetry-api as a dependency and instrument
BaseSession.send_request() with a CLIENT span per outgoing request.

This establishes the tracing foundation for the SDK. Each request
gets a span with the MCP method name and, where applicable, the
target name (tool name, prompt name, resource URI). Spans record
OK/ERROR status and error type attributes. Excluded methods
(e.g. notifications/message) are skipped.
Comment on lines 1 to 4
# pyright: reportMissingImports=false, reportUnknownVariableType=false
# pyright: reportUnknownMemberType=false, reportUnknownParameterType=false
# pyright: reportUnknownArgumentType=false
# opentelemetry-sdk does not ship type stubs, so we suppress unknown-type errors.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems it does. I can see the py.typed.

# Store the callback for this request
self._progress_callbacks[request_id] = progress_callback

method = request_data.get("method", "")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case that we don't have a method?

Comment on lines 35 to 39
def _build_span_name(method: str, target: str | None) -> str:
"""Build a span name like 'tools/call my_tool' or just 'ping'."""
if target:
return f"{method} {target}"
return method
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this utility.

Comment on lines 134 to 136
async with create_client_server_memory_streams() as (client_streams, server_streams):
client_read, client_write = client_streams
server_read, server_write = server_streams
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use mcp.client.Client instead?

Comment on lines 66 to 74
@pytest.mark.parametrize(
("method", "target", "expected"),
[
("tools/call", "my_tool", "tools/call my_tool"),
("ping", None, "ping"),
],
)
def test_build_span_name(method: str, target: str | None, expected: str) -> None:
assert _build_span_name(method, target) == expected
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't test private functions! Please document that in the CLAUDE.md.

Comment on lines 51 to 63
@pytest.mark.parametrize(
("method", "params", "expected"),
[
("tools/call", {"name": "my_tool"}, "my_tool"),
("prompts/get", {"name": "my_prompt"}, "my_prompt"),
("resources/read", {"uri": "file:///a.txt"}, "file:///a.txt"),
("ping", None, None),
("tools/call", {}, None),
("tools/call", {"name": 123}, None),
],
)
def test_extract_target(method: str, params: dict[str, Any] | None, expected: str | None) -> None:
assert _extract_target(method, params) == expected
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't test private functions! Please document that in the CLAUDE.md.

- Remove pyright suppressions (opentelemetry-sdk ships py.typed)
- Use `request_data["method"]` instead of `.get("method", "")`
- Inline `_build_span_name` into `start_client_span`
- Remove unit tests for private functions, test through E2E instead
- Rewrite error test to use `Client` (timeout-based)
- Add "do not test private functions" rule to CLAUDE.md
# Store the callback for this request
self._progress_callbacks[request_id] = progress_callback

method: str = request_data["method"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the cast needed?

@claude
Copy link

claude bot commented Feb 10, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Instrument the server side of MCP request handling with OpenTelemetry
SERVER spans. The span starts after request validation in _receive_loop
and ends when RequestResponder.respond() or cancel() is called, covering
the full server-side request lifecycle.
@Kludex Kludex changed the title feat: add OpenTelemetry tracing for client requests feat: add OpenTelemetry tracing for client and server requests Feb 10, 2026
Comment on lines 368 to 371
server_span = start_server_span(
request_data.get("method", ""),
request_data.get("params"),
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single line please. Is the method not always available in request_data?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant