Add OpenTelemetry tracing and metrics for MCP tool calls#24
Add OpenTelemetry tracing and metrics for MCP tool calls#24diego-ferrand wants to merge 1 commit into
Conversation
| def _get_meta(ctx: Any) -> dict: | ||
| try: | ||
| return ctx.request_context.request.params.meta or {} | ||
| except Exception: | ||
| return {} | ||
|
|
||
|
|
||
| def _extract_trace_context(meta: dict): | ||
| if not meta: | ||
| return None | ||
| try: | ||
| from opentelemetry.propagate import extract | ||
| carrier = {} | ||
| if "traceparent" in meta: | ||
| carrier["traceparent"] = meta["traceparent"] | ||
| if "tracestate" in meta: | ||
| carrier["tracestate"] = meta["tracestate"] | ||
| return extract(carrier) if carrier else None | ||
| except Exception: | ||
| return None |
There was a problem hiding this comment.
Hey @diego-ferrand I was interiorizing with the PR and I got a pretty big alert on how are we handling the meta withing the context.
This is so far the research I could put through:
_extract_trace_context never extracts the parent context, so server spans are never linked to the caller's trace. Local spans/metrics still work, which is why this
fails quietly rather than erroring.
Root cause: request.params.meta is not a dict — it's the SDK's RequestParams.Meta Pydantic model (alias="_meta", extra="allow"). The _meta → .meta
mapping is handled correctly by the alias, but traceparent/tracestate arrive as model attributes, not dict keys. The current code does dict-style access on it:
if "traceparent" in meta: # always False on a BaseModel
carrier["traceparent"] = meta["traceparent"] # 'Meta' object is not subscriptableSo carrier is always empty → extract() is never called → parent_ctx is always None.
Reproduced against the real SDK type:
type(meta): mcp.types.RequestParams.Meta
"traceparent" in meta => False
meta["traceparent"] => TypeError: 'Meta' object is not subscriptable
_extract_trace_context => None
Suggested fix — normalize to a dict first (handles both the Pydantic-model and plain-dict shapes, so we don't have to assume the client/transport):
def _extract_trace_context(meta):
if not meta:
return None
if hasattr(meta, "model_dump"):
meta = meta.model_dump()
...model_dump() surfaces the extras correctly:
{'progressToken': None, 'traceparent': '00-...-01', 'tracestate': 'rojo=...'}
Tests: tests/test_telemetry.py currently has no coverage for traceparent / _get_meta / _extract_trace_context, which is how this regressed without anything
failing. A MagicMock ctx would fake in/[] and mask the bug, so the test should build the actual SDK type.
Non-breaking for everything else, but blocking for the OTEL propagation goal — it defeats the whole point of passing traceparent.
No description provided.