feat(mcp): add native OpenTelemetry observability - #3041
Open
fahreddinozcan wants to merge 7 commits into
Open
Conversation
fahreddinozcan
marked this pull request as ready for review
August 16, 2026 11:34
There was a problem hiding this comment.
Pull request overview
This PR adds first-class OpenTelemetry observability to the @upstash/context7-mcp server by instrumenting MCP operations at the SDK transport boundary (including batched JSON-RPC messages), exporting MCP server spans/metrics, and exposing Prometheus/OpenMetrics on a dedicated internal listener.
Changes:
- Add MCP operation/session instrumentation (
SERVERspans +mcp.server.operation.duration/mcp.server.session.duration) with SEP-414 context extraction fromparams._meta. - Add bounded application metrics for upstream Context7 API calls, tool outcomes, authentication outcomes, and Node runtime signals, plus an embedded Prometheus exporter on
:9464/metrics. - Add shutdown handling for stdio to close/flush reliably, and add tests covering lifecycle, disabled-path behavior, batching, and exporter behavior.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-lock.yaml |
Locks new OpenTelemetry dependencies used by the MCP package. |
packages/mcp/package.json |
Adds OpenTelemetry API/SDK/exporter and runtime instrumentation dependencies. |
packages/mcp/Dockerfile |
Exposes the dedicated Prometheus metrics port (9464) in the container image. |
packages/mcp/src/index.ts |
Wires telemetry into HTTP + stdio serving paths, adds auth/upstream observation, and conditional embedded exporter bootstrapping. |
packages/mcp/src/lib/api.ts |
Wraps Context7 upstream calls with observeUpstreamRequest and adds tool outcome classification for context fetches. |
packages/mcp/src/lib/types.ts |
Extends ContextResponse with a bounded outcome for tool telemetry. |
packages/mcp/src/lib/tool-names.ts |
Centralizes tool names and defines bounded ToolCallOutcome. |
packages/mcp/src/lib/telemetry.ts |
Implements bounded upstream/auth metrics, tool outcome tagging, and upstream error classification. |
packages/mcp/src/lib/telemetry-config.ts |
Adds env-based switches for hard-off telemetry and embedded Prometheus enablement. |
packages/mcp/src/lib/telemetry-provider.ts |
Implements embedded Prometheus MetricReader + Node runtime metrics bootstrap with “fail open” behavior. |
packages/mcp/src/lib/mcp-telemetry.ts |
Adds transport-boundary MCP operation/session instrumentation and span/metric classification. |
packages/mcp/src/lib/mcp-operation-scope.ts |
Adds AsyncLocalStorage-based per-operation scope for tool outcome/error marking. |
packages/mcp/src/lib/stdio-shutdown.ts |
Adds idempotent stdio shutdown with bounded flush behavior. |
packages/mcp/README.md |
Documents signals, env configuration, scrape endpoint, and gateway non-duplication guidance. |
packages/mcp/test/telemetry.test.ts |
Unit tests for method/tool normalization, SEP-414 extraction, config flags, and span parenting/linking. |
packages/mcp/test/telemetry-disabled.test.ts |
Verifies OTEL_SDK_DISABLED=true yields no exported application metrics. |
packages/mcp/test/stdio-shutdown.test.ts |
Tests idempotent shutdown, error handling, and flush timeout behavior. |
packages/mcp/test/mcp-telemetry-lifecycle.test.ts |
Exercises MCP lifecycle classification, cancellations, transport errors, and session metric behavior. |
packages/mcp/test/integration.test.ts |
End-to-end validation of exported Prometheus series (batch counts, bounded labels, runtime metrics, exporter collision). |
.changeset/clean-otters-observe.md |
Publishes a minor release note for the new observability features. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mcp.server.operation.durationandmcp.server.session.duration, plusSERVERspans with standard MCP/GenAI/JSON-RPC/network attributestraceparent,tracestate, andbaggagefromparams._metaper SEP-414, using the extracted context as parent and linking ambient HTTP context:9464/metricslistener while supporting externally preloaded OpenTelemetry providersOTEL_SDK_DISABLED=trueas a true hard-off boundary: no telemetry provider modules, transport wrappers, async scopes, spans, metrics, or runtime collectors are loadedWhy this integration
The MCP TypeScript SDK v2.0.0 does not ship a turnkey OpenTelemetry plugin, tracer/meter, exporter, or completed-operation middleware. It does expose SEP-414 propagation constants and the public transport boundary. Instrumenting that boundary observes protocol operations across HTTP and stdio without subclassing protected SDK internals or mistaking an HTTP envelope for a single MCP operation.
The cluster already has gateway HTTP and Kubernetes/container signals, but those cannot identify MCP methods, individual tool health, authentication outcomes, Context7 API dependency failures, or Node event-loop/heap pressure. This adds those missing bounded semantic signals without duplicating gateway request telemetry.
Envoy non-duplication audit
The checked-in production data path is Envoy Gateway ->
HTTPRoute/mcp->Service/mcp-svc-> MCP pods. Envoy is not a sidecar in the MCP pod.envoy_http_*_downstream_*envoy_cluster_upstream_*mcp_server_operation_duration_countis a JSON-RPC operation count after SDK dispatch, not an HTTP request count: one valid batch is one Envoy request and multiple MCP operations; pre-dispatch HTTP rejections are Envoy-onlycontext7_mcp_upstream_*observes MCP-to-Context7 API calls, which the ingress gateway cannot seeSignals
mcp_server_operation_duration— standard MCP server operation histogram;_countis the operation count. Tool calls use standardgen_ai.tool.nameplus boundedcontext7.mcp.tool.outcome(success,not_found, orerror), avoiding a duplicate tool counter/histogram.mcp_server_session_duration— standard MCP session histogram for real stateful stdio sessions; stateless HTTP request transports are excludedSERVERspans with SEP-414 parent extraction and ambient transport linkscontext7_mcp_operations_activecontext7_mcp_upstream_requests_total,context7_mcp_upstream_request_duration, andcontext7_mcp_upstream_requests_activecontext7_mcp_authentication_attempts_total,context7_mcp_authentication_duration, andcontext7_mcp_authentication_activenodejs_eventloop_*,v8js_gc_duration,v8js_memory_heap_*, andv8js_resource_activetarget_inforesource metadataUpstream outcomes distinguish HTTP, response-decoding, network, timeout, and cancellation failures; they include both bounded status class and exact numeric HTTP status. Authentication distinguishes accepted, missing, invalid, and unexpected-error outcomes.
Labels exclude API keys, IPs, client versions, queries, library IDs, session IDs, tool arguments/results, and raw error text. Server-side JSON-RPC caller faults remain visible through
rpc.response.status_codewithout settingerror.type; internal, transport, and tool failures set bounded error types.Operations
0.0.0.0:9464/metricsOTEL_EXPORTER_PROMETHEUS_HOSTandOTEL_EXPORTER_PROMETHEUS_PORTconfigure the listenerOTEL_METRICS_EXPORTER=nonedisables only the embedded exporter, allowing a preloaded provider to receive signalsOTEL_SDK_DISABLED=trueentirely bypasses instrumentation and provider bootstrapMeterProvider/TracerProvidertakes precedence; external instrumentation owns Node runtime registration in that modeThe deployment repository still needs to expose port
9464internally and add the Prometheus scrape/ServiceMonitor; it should not route this port through public MCP ingress.Resource-overhead benchmark
Local Docker benchmark against the merge-base image, using an in-memory/stubbed upstream to make telemetry cost maximally visible: 8 concurrent MCP
query-docsclients, 10-second warm-up, 30-second measurement, three repetitions, rotated mode order, and 15-second Prometheus scraping when enabled. Every measured call succeeded.OTEL_SDK_DISABLED=trueMedian absolute CPU cost was
0.546 ms/opfor baseline,0.549 ms/opdisabled, and0.639 ms/openabled. Median throughput was 1815, 1801, and 1803 operations/second respectively. The enabled result is intentionally a worst-case CPU-saturated local test with essentially no upstream latency; production's network-bound requests should have a lower relative percentage, while the absolute memory and CPU measurements remain the useful capacity-planning bounds.The production image grew from 115,663,904 to 118,468,819 bytes: +2,804,915 bytes (+2.43%).
Validation
pnpm typecheck— passedpnpm lint:check— passedpnpm format:check— passedpnpm build— passedpnpm test— 9 files / 112 tests passednot_found, authentication timing/concurrency, runtime metrics, batch counts, active-gauge cleanup, and exporter collisiontools/calloperation series exist and removedcontext7_mcp_tool_calls_*duplicate series do notReferences