Skip to content

Support W3C Trace Context Propagation via _meta per SEP-414#397

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:otel_trace_context_meta
Open

Support W3C Trace Context Propagation via _meta per SEP-414#397
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:otel_trace_context_meta

Conversation

@koic

@koic koic commented Jun 12, 2026

Copy link
Copy Markdown
Member

Motivation and Context

SEP-414 (modelcontextprotocol/modelcontextprotocol#414, merged for the 2026-07-28 spec release) documents OpenTelemetry trace context propagation conventions for MCP: the un-prefixed _meta keys traceparent, tracestate, and baggage are reserved (an explicit exception to the reverse-DNS prefix rule) so trace context can flow between MCP clients and servers per the W3C Trace Context and Baggage specifications.

This follows the TypeScript SDK's approach (typescript-sdk#2270): guarantee _meta passthrough, export constants for the reserved key names, and lock the behavior in with regression tests and documentation, without adding an OpenTelemetry dependency (the Python SDK's python-sdk#2381 makes opentelemetry-api a hard dependency instead, which conflicts with this SDK's keep-dependencies-minimal policy):

  • New MCP::TraceContext module exposing TRACEPARENT_META_KEY, TRACESTATE_META_KEY, BAGGAGE_META_KEY, and META_KEYS.
  • The server side already passes incoming request _meta to tool and prompt handlers untouched via server_context[:_meta]; new regression tests pin that guarantee for the three reserved keys.
  • Every MCP::Client request method (call_tool, read_resource, get_prompt, complete, ping, and the list_* methods) gains a meta: keyword so Ruby clients can inject trace context (or any other _meta entries) into outgoing requests; per SEP-414, trace context should flow on every request, not only tool calls. The caller's hash is never mutated. On call_tool, progress_token takes precedence over a progressToken entry in meta, and an empty merge result omits _meta entirely, preserving the existing wire format.
  • README documents the convention and how to bridge the values to a tracing system such as the opentelemetry-ruby gems.

Scope is intentionally client-to-server: server-initiated requests (sampling/createMessage, roots/list, elicitation/create) do not accept meta: yet; propagating trace context on those is a separate follow-up.

Resolves #374.

How Has This Been Tested?

  • New test/mcp/trace_context_test.rb pins the exact reserved key names and the frozen META_KEYS list.
  • New tests in test/mcp/server_test.rb assert that tools/call and prompts/get deliver traceparent, tracestate, and baggage (alongside progressToken) to handlers through server_context[:_meta] unchanged.
  • New tests in test/mcp/client_test.rb cover the meta: keyword: the trace keys are sent in _meta on call_tool and read_resource, a table-driven test exercises the remaining request methods (tools/list, resources/list, resources/templates/list, prompts/list, prompts/get, completion/complete, ping), progress_token overrides a progressToken entry in meta without duplicating the key, the caller's meta hash is not mutated, and requests without meta: serialize exactly as before (wire-format regressions for both the empty meta hash and the no-params list request).

Breaking Changes

None. MCP::TraceContext and the meta: keyword are purely additive, and requests without meta: serialize exactly as before.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

SEP-414 (modelcontextprotocol/modelcontextprotocol#414, merged for
the 2026-07-28 spec release) documents OpenTelemetry trace context propagation
conventions for MCP: the un-prefixed `_meta` keys `traceparent`,
`tracestate`, and `baggage` are reserved (an explicit exception to
the reverse-DNS prefix rule) so trace context can flow between MCP clients and
servers per the W3C Trace Context and Baggage specifications.

This follows the TypeScript SDK's approach (typescript-sdk#2270):
guarantee `_meta` passthrough, export constants for the reserved key names,
and lock the behavior in with regression tests and documentation, without adding
an OpenTelemetry dependency (the Python SDK's python-sdk#2381 makes
`opentelemetry-api` a hard dependency instead, which conflicts with
this SDK's keep-dependencies-minimal policy):

- New `MCP::TraceContext` module exposing `TRACEPARENT_META_KEY`,
  `TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`.
- The server side already passes incoming request `_meta` to tool and prompt
  handlers untouched via `server_context[:_meta]`; new regression tests pin
  that guarantee for the three reserved keys.
- Every `MCP::Client` request method (`call_tool`, `read_resource`,
  `get_prompt`, `complete`, `ping`, and the `list_*` methods) gains
  a `meta:` keyword so Ruby clients can inject trace context (or any other
  `_meta` entries) into outgoing requests; per SEP-414, trace context
  should flow on every request, not only tool calls. The caller's hash is
  never mutated. On `call_tool`, `progress_token` takes precedence over
  a `progressToken` entry in `meta`, and an empty merge result omits `_meta`
  entirely, preserving the existing wire format.
- README documents the convention and how to bridge the values to a tracing
  system such as the `opentelemetry-ruby` gems.

Scope is intentionally client-to-server: server-initiated requests
(`sampling/createMessage`, `roots/list`, `elicitation/create`) do not accept
`meta:` yet; propagating trace context on those is a separate follow-up.

Resolves modelcontextprotocol#374.

## How Has This Been Tested?

- New `test/mcp/trace_context_test.rb` pins the exact reserved key names and
  the frozen `META_KEYS` list.
- New tests in `test/mcp/server_test.rb` assert that `tools/call` and
  `prompts/get` deliver `traceparent`, `tracestate`, and `baggage` (alongside
  `progressToken`) to handlers through `server_context[:_meta]` unchanged.
- New tests in `test/mcp/client_test.rb` cover the `meta:` keyword: the
  trace keys are sent in `_meta` on `call_tool` and `read_resource`,
  a table-driven test exercises the remaining request methods (`tools/list`,
  `resources/list`, `resources/templates/list`, `prompts/list`,
  `prompts/get`, `completion/complete`, `ping`), `progress_token` overrides
  a `progressToken` entry in `meta` without duplicating the key,
  the caller's `meta` hash is not mutated, and requests without `meta:`
  serialize exactly as before (wire-format regressions for both the empty `meta`
  hash and the no-params list request).

## Breaking Changes

None. `MCP::TraceContext` and the `meta:` keyword are purely additive, and
requests without `meta:` serialize exactly as before.
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.

SEP-414: Document OpenTelemetry Trace Context Propagation Conventions

1 participant