Skip to content

feat(server): add WithMeter for OTEL-style metrics on requests and tools - #893

Draft
QuentinBisson wants to merge 1 commit into
mark3labs:mainfrom
QuentinBisson:feat/server-with-meter
Draft

feat(server): add WithMeter for OTEL-style metrics on requests and tools#893
QuentinBisson wants to merge 1 commit into
mark3labs:mainfrom
QuentinBisson:feat/server-with-meter

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented May 18, 2026

Copy link
Copy Markdown
Contributor

Why

The server has `WithTracer` for OTEL spans on every dispatched JSON-RPC method (#856) but no symmetric primitive for metrics. Authors who want per-method or per-tool latency histograms today wrap individual tool handlers in their own `ToolHandlerMiddleware`, which only sees `tools/call` and never `initialize`, `tools/list`, `resources/read`, etc.

What

Three changes, all mirroring the existing `WithTracer` shape:

  1. `metrics/` package — abstract `Meter` / `Counter` / `Histogram` interfaces with a noop default. mcp-go itself takes no OTEL (or any specific metrics library) dependency, mirroring how the `tracing` package abstracts `Tracer` / `Span`.

  2. `server.WithMeter(metrics.Meter) ServerOption` — installs the meter and registers four instruments:

    Instrument Kind Unit Attributes
    `mcp.request.calls` counter `{call}` `mcp.method`, `mcp.session.id` (when set), `mcp.protocol.version` (from `Mcp-Protocol-Version`), `outcome` (`ok`|`error`)
    `mcp.request.duration` histogram `s` same
    `mcp.tool.calls` counter `{call}` `mcp.tool.name`, `outcome` (`ok`|`error`|`error_result`)
    `mcp.tool.duration` histogram `s` same

    When a Tracer is also installed via `WithTracer`, the OTEL SDK attaches exemplars carrying the active span's TraceID/SpanID to histogram observations (default `TraceBasedFilter` does this automatically), enabling "click latency bucket → jump to trace" pivots in Grafana / Tempo.

  3. `mcp-go/otel` adapter — gains `WithServerMetrics(otelmetric.Meter)` and `NewMeter`, mirroring the existing `WithServerTracing` / `NewTracer` pair.

Design parity with WithTracer

Concern `WithTracer` (existing) `WithMeter` (this PR)
Server option `WithTracer(tracing.Tracer)` `WithMeter(metrics.Meter)`
Per-method hook `startMessageSpan` in `request_handler.go` `startMessageMetric` in `request_handler.go`
Tool-level `toolTracingMiddleware` auto-registered `toolMetricsMiddleware` auto-registered
Nil handling nil tracer → no-op nil meter → no-op
Abstract package `tracing/` `metrics/`
OTEL adapter `otel.WithServerTracing` `otel.WithServerMetrics`
Attribute names `mcp.method`, `mcp.tool.name`, `mcp.session.id`, `mcp.protocol.version` identical, plus `outcome`

Validation

  • `go test ./metrics/` clean — two tests for noop + Attribute helper.
  • `go test ./server/` clean — four new tests covering nil-noop, request-line OK and error outcomes, and tool middleware across the three outcomes (ok, handler error, IsError result).
  • `go test ./otel/` clean — `NewMeter(nil)` no-op + end-to-end counter/histogram round-trip through the OTEL SDK's manual reader.

@mark-iii-labs-huly

Copy link
Copy Markdown

Connected to Huly®: MCP_G-458

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dd669266-c54c-4203-b152-e4b41d79596f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@QuentinBisson
QuentinBisson force-pushed the feat/server-with-meter branch from ddf0bd3 to 2388bb5 Compare May 18, 2026 12:45
Mirror WithTracer's design for metrics. A new abstract metrics/ package
exposes Meter / Counter / Histogram / Attribute interfaces with a noop
default, so mcp-go itself stays free of any OTEL dependency. A new
server.WithMeter(metrics.Meter) installs the meter and registers the
following instruments:

  - mcp.request.calls (counter, "{call}") with attributes mcp.method,
    mcp.session.id (when set), mcp.protocol.version (from the
    Mcp-Protocol-Version header), outcome (ok|error).
  - mcp.request.duration (histogram, "s") with the same attributes.
  - mcp.tool.calls (counter, "{call}") with attributes mcp.tool.name,
    outcome (ok|error|error_result).
  - mcp.tool.duration (histogram, "s") with the same attributes.

When a Tracer is also installed via WithTracer, the OTEL SDK attaches
exemplars carrying the active span's TraceID/SpanID to histogram
observations (the SDK does this automatically with its default
TraceBasedFilter), enabling "click latency bucket → jump to trace"
pivots in Grafana / Tempo.

The OpenTelemetry adapter at github.com/mark3labs/mcp-go/otel grows
WithServerMetrics(otelmetric.Meter) plus a NewMeter helper, matching
the existing tracer adapter API.
@QuentinBisson
QuentinBisson force-pushed the feat/server-with-meter branch from 2388bb5 to 39b2f2e Compare May 18, 2026 13:45
@QuentinBisson

Copy link
Copy Markdown
Contributor Author

@ezynda3 — would love your read on the design before this goes further. Two questions:

  1. Abstract `metrics` package vs. direct OTEL. I followed the same shape `tracing/` uses — a small `Meter` / `Counter` / `Histogram` / `Attribute` interface in `metrics/` with a noop default, plus an adapter in `mcp-go/otel` that wraps `otelmetric.Meter`. Mirror of how `tracing.Tracer` keeps mcp-go free of OTEL as a hard dependency. The alternative would be to take `metric.Meter` directly on `server.WithMeter` and not introduce `metrics/` — simpler API surface, but ties the main module to OTEL. I lean toward the current shape for parity with tracing; happy to flip if you prefer.

  2. Instrument names and units. I went with `mcp.request.calls` (counter, `{call}`) + `mcp.request.duration` (histogram, `s`) for the JSON-RPC dispatch layer, and `mcp.tool.calls` + `mcp.tool.duration` for the tool-handler middleware. Attributes (`mcp.method`, `mcp.tool.name`, `mcp.session.id`, `mcp.protocol.version`, `outcome`) match what `WithTracer` already emits. If you have a naming convention in mind, easy to adjust.

The instrument set is opinionated but small enough that splitting (request vs tool, counter vs histogram) is straightforward if you'd rather land just one slice first.

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