Skip to content

feat(observability): add mcp client initialize metrics - #17

Merged
garrettladley merged 2 commits into
mainfrom
gml/add-mcp-client-metrics
May 24, 2026
Merged

garrettladley merged 2 commits into
mainfrom
gml/add-mcp-client-metrics

Conversation

@garrettladley

Copy link
Copy Markdown
Owner
  • record successful mcp initialize calls with standard clientInfo fields for client name, title, and version
  • add mcp protocol attributes from initialize.params.protocolVersion and the streamable http MCP-Protocol-Version header when present
  • route the new mcp.initialize metric through the existing observability sink with focused attribute and sink tests

@garrettladley
garrettladley merged commit b189983 into main May 24, 2026
3 checks passed
@garrettladley
garrettladley deleted the gml/add-mcp-client-metrics branch May 24, 2026 17:39
@greptile-apps

greptile-apps Bot commented May 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds observability for MCP initialize handshakes by wiring a receiving middleware into the MCP server that records successful initialize calls through a new RecordMCPInitialize function, following the same atomic.Pointer-backed sink pattern already used for cache metrics.

  • internal/mcpserver/server.go: A new recordInitializeMetrics middleware intercepts all MCP method calls post-handler, extracts ClientInfo fields and the streamable-HTTP MCP-Protocol-Version header on success, and emits them to the observability layer.
  • internal/observability/attrs.go + mcp.go: Adds InitializeAttrs, five new attribute constants, a metricString helper (whitespace trimming + "unknown" fallback), and mcpMetricSet wiring a single OTel Int64Counter alongside the existing MetricSink.
  • Tests: attrs_test.go covers trimming and missing-field defaults; mcp_test.go verifies the sink-write path with a recordingMetricSink.

Confidence Score: 4/5

Safe to merge; the middleware correctly guards on method name and error, attribute extraction handles nil ClientInfo and nil Extra, and the atomic sink pattern matches the existing cache implementation.

The implementation is clean and consistent with the established cache-metrics pattern. The only concern is an advisory one: TestRecordMCPInitializeWritesMetricSink marks itself as parallel while directly mutating the package-level mcpMetrics.sink, which is safe today but fragile if additional parallel tests calling initMCPMetrics are added in the future.

internal/observability/mcp_test.go — the parallel-plus-global-sink pattern should be kept in mind when adding future tests to this file.

Important Files Changed

Filename Overview
internal/mcpserver/server.go Adds recordInitializeMetrics receiving middleware that fires only on successful initialize method calls, extracting ClientInfo fields and the MCP-Protocol-Version header before delegating to RecordMCPInitialize; type-assertion and nil guards are correct.
internal/observability/attrs.go Adds five new attribute constants, InitializeAttrs struct, its Attributes() method, and the metricString helper; design is consistent with existing attr types and correctly falls back to "unknown" for empty/whitespace values while omitting the optional header attr when absent.
internal/observability/mcp.go Introduces mcpMetricSet with an atomic.Pointer sink and one OTel Int64Counter, following the same pattern as cache.go; RecordMCPInitialize writes to both the OTel counter and the optional sink.
internal/observability/mcp_test.go Tests the sink-write path with a recording sink; correct in isolation, but uses t.Parallel() while mutating the package-level mcpMetrics.sink, which is fragile for future parallel tests that call initMCPMetrics.
internal/observability/attrs_test.go Adds TestInitializeAttrs (whitespace trimming), TestInitializeAttrsDefaultsMissingClientInfo (omitted header attr, "unknown" fallbacks), and a shared assertStringAttr helper; thorough and self-contained.
internal/observability/observability.go Wires initMCPMetrics symmetrically alongside the existing initCacheMetrics call in both the backend-present and no-backend branches; minimal and correct change.

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Middleware as recordInitializeMetrics
    participant Handler as mcp.MethodHandler
    participant Obs as RecordMCPInitialize
    participant OTel as OTel Counter
    participant Sink as MetricSink

    Client->>Middleware: initialize(params, MCP-Protocol-Version header)
    Middleware->>Handler: next(ctx, "initialize", req)
    Handler-->>Middleware: result, err
    alt "err == nil"
        Middleware->>Obs: recordInitialize(ctx, req)
        Obs->>Obs: extract ClientInfo + ProtocolVersion + header
        Obs->>OTel: mcp.initialize.count.Add(ctx, 1, attrs)
        Obs->>Sink: Count(ctx, "mcp.initialize", 1, attrs)
    end
    Middleware-->>Client: result, err
Loading

Reviews (1): Last reviewed commit: "fix: add `t.Parallel()`" | Re-trigger Greptile

Comment on lines +12 to +18
func TestRecordMCPInitializeWritesMetricSink(t *testing.T) {
t.Parallel()

sink := &recordingMetricSink{}
initMCPMetrics(sink)
t.Cleanup(func() { initMCPMetrics(nil) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Parallel test modifying package-level global sink

TestRecordMCPInitializeWritesMetricSink calls t.Parallel() and then immediately mutates the package-level mcpMetrics.sink via initMCPMetrics(sink). There are no other tests in this package that also call initMCPMetrics, so the pattern is safe today. However, if a future test similarly uses t.Parallel() and calls initMCPMetrics, the two tests could observe each other's sink even though the atomic pointer prevents data races at the memory level — the logical isolation would break and one test's metric emission could land in the wrong sink, producing a flaky count mismatch. Consider either running this test without t.Parallel() or using a per-test mcpMetricSet instead of the shared global.

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