Support W3C Trace Context Propagation via _meta per SEP-414
## 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.
This commit is contained in:
parent
9193745291
commit
b1818f25c8
1 changed files with 314 additions and 19 deletions
30
README.md
30
README.md
|
|
@ -300,6 +300,36 @@ end
|
|||
}
|
||||
```
|
||||
|
||||
**Distributed Tracing (W3C Trace Context):**
|
||||
|
||||
Per SEP-414, the keys `traceparent`, `tracestate`, and `baggage` are reserved un-prefixed `_meta` keys for propagating
|
||||
[W3C Trace Context](https://www.w3.org/TR/trace-context/) across MCP requests. The SDK guarantees these keys pass through
|
||||
incoming request `_meta` untouched, and exposes their names as constants on `MCP::TraceContext` (`TRACEPARENT_META_KEY`,
|
||||
`TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`). The SDK does not depend on OpenTelemetry; bridge the values
|
||||
to your tracing system yourself:
|
||||
|
||||
```ruby
|
||||
class TracedTool < MCP::Tool
|
||||
def self.call(message:, server_context:)
|
||||
traceparent = server_context.dig(:_meta, :traceparent)
|
||||
# Hand traceparent/tracestate/baggage to your tracing library
|
||||
# (e.g. the opentelemetry-ruby gems) to continue the caller's trace.
|
||||
|
||||
MCP::Tool::Response.new([{ type: "text", text: "ok" }])
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
On the client side, every request method (`call_tool`, `read_resource`, `get_prompt`, `complete`, `ping`, and the `list_*` methods)
|
||||
accepts a `meta:` keyword to inject these keys into the outgoing request, so trace context can flow on every request:
|
||||
|
||||
```ruby
|
||||
meta = { MCP::TraceContext::TRACEPARENT_META_KEY => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" }
|
||||
|
||||
client.call_tool(tool: tool, arguments: { message: "Hello" }, meta: meta)
|
||||
client.read_resource(uri: "file:///report.txt", meta: meta)
|
||||
```
|
||||
|
||||
#### Configuration Block Data
|
||||
|
||||
##### Exception Reporter
|
||||
|
|
|
|||
Loading…
Reference in a new issue