feat: implement OTEL mcp.server.* metrics#2394
feat: implement OTEL mcp.server.* metrics#2394verdie-g wants to merge 7 commits intomodelcontextprotocol:mainfrom
Conversation
…duration metrics Implements the two server-side OTel metrics defined in the MCP semantic conventions (https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp), as discussed in modelcontextprotocol#421. Changes: - src/mcp/shared/_otel.py: add a meter and two histograms with the spec-mandated explicit bucket boundaries [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300]. Expose record_server_operation_duration() and record_server_session_duration() helpers. - src/mcp/server/lowlevel/server.py: record mcp.server.operation.duration in _handle_request() on all exit paths (success, MCPError, cancellation, transport close, raise_exceptions=True). Attributes recorded: mcp.method.name (required), error.type, rpc.response.status_code, gen_ai.tool.name + gen_ai.operation.name (tools/call), gen_ai.prompt.name (prompts/get), mcp.protocol.version. - src/mcp/server/session.py: record mcp.server.session.duration in __aexit__(). Session start time is captured in __aenter__() (not __init__()) so the clock starts when the session is actually active. Cancellation exceptions (transport close) are not treated as errors. - tests/shared/test_otel.py: extend test_client_and_server_instrumentation to assert both metrics are emitted with correct attributes and units. Transport attributes (network.transport, network.protocol.name, network.protocol.version) are not set. ServerSession does not currently know the transport type — adding it would require a new constructor parameter and a corresponding API change. This can be addressed in a follow-up once the transport kind is plumbed through. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
a3994ec to
deb20cc
Compare
Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Fix corrupted test_otel.py (stray ======= artifacts) - Add test_server_operation_error_metrics: MCPError sets error.type and rpc.response.status_code - Add test_server_session_error_metrics: session crash sets error.type on session duration - Restore missing session_point count/sum assertions - 100% coverage on _otel.py, server.py, session.py Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Now covered by test_server_session_error_metrics. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
time.monotonic() on Windows can have low enough resolution that fast in-memory calls measure as 0.0s. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
explicit_bucket_boundaries_advisory was added in 1.30.0. The previous minimum of 1.28.0 caused TypeError in CI. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Bump logfire>=3.20.0: fixes _ProxyMeter not forwarding explicit_bucket_boundaries_advisory kwarg (fixed in 3.20.0) - Replace capfire.get_collected_metrics() (logfire>=4.0.0 only) with metrics_reader.get_metrics_data() which works from 3.0.0 - Fix pre-existing pyright reportUnknownMemberType errors in test_streamable_http.py exposed by the logfire version bump Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
| "typing-inspection>=0.4.1", | ||
| "opentelemetry-api>=1.28.0", | ||
| "opentelemetry-api>=1.30.0", | ||
| ] |
There was a problem hiding this comment.
Bumped from 1.28.0: explicit_bucket_boundaries_advisory was added to Meter.create_histogram() in opentelemetry-api 1.30.0. The previous minimum caused a TypeError at runtime when the OTel proxy meter replayed histogram creation against the real meter.
| "strict-no-cover", | ||
| "logfire>=3.0.0", | ||
| "logfire>=3.20.0", | ||
| ] |
There was a problem hiding this comment.
Bumped from 3.0.0: logfire's _ProxyMeter.create_histogram() did not forward unknown kwargs (including explicit_bucket_boundaries_advisory) until 3.20.0, causing a TypeError when logfire was configured as the meter provider. Also replaced capfire.get_collected_metrics() (added in logfire 4.0.0) with capfire.metrics_reader.get_metrics_data() which works from 3.0.0, see _get_mcp_metrics in test_otel.py.
|
|
||
|
|
||
| def _get_mcp_metrics(capfire: CaptureLogfire) -> dict[str, Any]: | ||
| """Return collected metrics whose name starts with 'mcp.', keyed by name.""" |
There was a problem hiding this comment.
CaptureLogfire.get_collected_metrics() was only added in logfire 4.0.0, but our minimum is 3.20.0. This helper replicates the same logic using capfire.metrics_reader.get_metrics_data(), which is available from logfire 3.0.0 (the metrics_reader field is part of the public CaptureLogfire dataclass).
Implements the two server-side OTel metrics defined in the MCP semantic conventions, as discussed in #421.
network.transport,network.protocol.name, andnetwork.protocol.versionare not set.ServerSessiondoes not currently know the transport type, adding it would require a new constructor parameter and a corresponding public API change. This can be addressed in a follow-up once the transport kind is plumbed through.