Skip to content

feat: implement OTEL mcp.server.* metrics#2394

Open
verdie-g wants to merge 7 commits intomodelcontextprotocol:mainfrom
verdie-g:otel-metrics
Open

feat: implement OTEL mcp.server.* metrics#2394
verdie-g wants to merge 7 commits intomodelcontextprotocol:mainfrom
verdie-g:otel-metrics

Conversation

@verdie-g
Copy link
Copy Markdown

@verdie-g verdie-g commented Apr 5, 2026

Implements the two server-side OTel metrics defined in the MCP semantic conventions, as discussed in #421.

network.transport, network.protocol.name, and 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 public API change. This can be addressed in a follow-up once the transport kind is plumbed through.

…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>
@verdie-g verdie-g changed the title feat: implement mcp.server.operation.duration and mcp.server.session.duration metrics feat: implement OTEL mcp.server.* metrics Apr 5, 2026
@verdie-g verdie-g force-pushed the otel-metrics branch 2 times, most recently from a3994ec to deb20cc Compare April 5, 2026 15:02
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
verdie-g and others added 5 commits April 5, 2026 18:04
- 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",
]
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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",
]
Copy link
Copy Markdown
Author

@verdie-g verdie-g Apr 5, 2026

Choose a reason for hiding this comment

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

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."""
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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).

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