Skip to content

feat(telemetry): optional OpenTelemetry instrumentation for MCP operations - #129

Open
getlarge wants to merge 8 commits into
platformatic:mainfrom
getlarge:feat/otel-instrumentation
Open

feat(telemetry): optional OpenTelemetry instrumentation for MCP operations#129
getlarge wants to merge 8 commits into
platformatic:mainfrom
getlarge:feat/otel-instrumentation

Conversation

@getlarge

@getlarge getlarge commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Closes #128.

Adds optional OpenTelemetry tracing for MCP server operations. There is no telemetry-path overhead when no tracer is configured; @opentelemetry/api remains an optional peer dependency and is loaded dynamically only when tracing is enabled.

What's included

  • telemetry?: { tracer: TracerLike } on MCPPluginOptions. TracerLike is structural, so consumers do not need @opentelemetry/api installed merely to import plugin types.
  • MCP requests and notifications are wrapped in active SERVER spans.
  • Span names follow {mcp.method.name} {target} for tool and prompt operations, for example tools/call echo.
  • Attributes follow the current MCP/GenAI conventions, including:
    • mcp.method.name, mcp.session.id, and mcp.protocol.version
    • gen_ai.tool.name, gen_ai.prompt.name, and gen_ai.operation.name
    • jsonrpc.request.id, rpc.response.status_code, and error.type
    • mcp.resource.uri where applicable
    • network.transport and HTTP network.protocol.* attributes
    • server-side client.address and client.port when available
  • W3C trace context and baggage are extracted from MCP params._meta through the configured OpenTelemetry propagator. When a remote MCP parent is present, the ambient HTTP span is linked rather than used as the MCP span parent.
  • Streamable HTTP/SSE is recorded as TCP (or QUIC when applicable) plus HTTP protocol attributes; the stdio adapter records network.transport = pipe.
  • JSON-RPC and tool errors update span attributes/status according to the MCP conventions. Successful spans remain UNSET rather than being forced to OK.
  • Tool arguments and results are not captured because those convention fields are opt-in and may contain sensitive data.
  • withSpan, buildSpanAttributes, MCP_ATTR, TracerLike, and HandlerDependencies are exported for advanced consumers.

Usage

import { trace } from '@opentelemetry/api'
import mcpPlugin from '@platformatic/mcp'

await app.register(mcpPlugin, {
  telemetry: { tracer: trace.getTracer('my-mcp-server', '1.0.0') },
})

Design note: local semantic-convention constants

The MCP conventions are still marked Development and now live in the OpenTelemetry GenAI semantic-conventions repository. The current convention uses gen_ai.tool.name and gen_ai.prompt.name; the older mcp.tool.name and mcp.prompt.name names are not used.

@opentelemetry/semantic-conventions does not provide the complete current set through its stable entry point. Its earlier MCP exports were incubating, and version 1.42 deprecated them after MCP/GenAI conventions moved to the separate GenAI repository. The package also recommends copying unstable definitions into instrumentation libraries instead of importing the incubating entry point at runtime.

For those reasons, this PR keeps the small set of required keys in src/telemetry-constants.ts, with a source link to the current GenAI MCP specification. This avoids adding a runtime semantic-conventions dependency while making convention drift explicit and reviewable.

Upstream reconciliation

The branch includes upstream main through v2.4.0 and preserves the newer tool authorization, task, and onToolCallComplete behavior alongside telemetry instrumentation.

Test plan

  • npm run typecheck
  • npm run lint
  • npm run test:types
  • npm run build
  • 78 focused telemetry, HTTP/stdio transport, protocol negotiation, authorization, in-process tool-call, and completion-hook tests
  • GitHub Actions green on Node.js 22 and 24

getlarge and others added 6 commits April 8, 2026 21:25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- withSpan() helper wraps fn execution in an active OTel span, loading
  @opentelemetry/api dynamically on first use so users without the peer
  dep pay zero cost
- MCP_ATTR constants and buildSpanAttributes() live in telemetry-constants.ts
  with no OTel dependency, so they can be imported statically by any module
- Attribute keys are inlined rather than sourced from
  @opentelemetry/semantic-conventions: the JS semconv package only exports
  4 of the 6 MCP attrs as of 1.40.0, under an explicitly unstable /experimental
  path. See telemetry-constants.ts for the full rationale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TracerLike is a structural interface compatible with @opentelemetry/api's
Tracer, defined locally so consumers don't need @opentelemetry/api installed
just to import plugin types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- handleRequest wraps each JSON-RPC dispatch in withSpan when a tracer
  is configured on HandlerDependencies
- Span names match the JSON-RPC method; span attributes use MCP semconv
  keys (mcp.method.name, mcp.session.id, plus mcp.tool.name /
  mcp.resource.uri / mcp.prompt.name for method-specific dispatches)
- The telemetry module is loaded lazily via dynamic import inside the
  per-request wrap, so users without a tracer configured never touch
  @opentelemetry/api
- Routes thread opts.telemetry?.tracer into processMessage
- HandlerDependencies is now exported for advanced consumers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export MCP_ATTR, buildSpanAttributes, withSpan, TracerLike, and
HandlerDependencies from the package root so advanced consumers can
build their own span wiring on top of the plugin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- test/telemetry.test.ts: unit tests for withSpan (success/error paths,
  no-tracer fallthrough) and buildSpanAttributes
- test/telemetry-types.test.ts: type-level tests for MCPPluginOptions.telemetry
- test/telemetry-integration.test.ts: end-to-end plugin -> tracer wiring,
  driving tools/call and tools/list through the HTTP surface and asserting
  span names, attributes, and per-method extras (mcp.tool.name)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@mcollina

Copy link
Copy Markdown
Member

Can you fix the conflicts?

…tation

# Conflicts:
#	package-lock.json
#	src/handlers.ts
#	src/routes/mcp.ts
#	src/types.ts
…tation

# Conflicts:
#	src/handlers.ts
#	src/types.ts
@getlarge

Copy link
Copy Markdown
Contributor Author

@getlarge
getlarge requested a review from mcollina August 22, 2026 13:15
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.

Add optional OpenTelemetry instrumentation for MCP operations

2 participants