feat(core): reserved trace context _meta keys and propagation docs (SEP-414) (#2270)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Matt 2026-06-15 13:32:08 +01:00 • committed by GitHub
commit 78fbe2736d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 330 additions and 5 deletions

View file

@ -71,6 +71,7 @@ export * from '../../types/types.js';
// Constants
export {
BAGGAGE_META_KEY,
CLIENT_CAPABILITIES_META_KEY,
CLIENT_INFO_META_KEY,
DEFAULT_NEGOTIATED_PROTOCOL_VERSION,
@ -84,7 +85,9 @@ export {
PARSE_ERROR,
PROTOCOL_VERSION_META_KEY,
RELATED_TASK_META_KEY,
SUPPORTED_PROTOCOL_VERSIONS
SUPPORTED_PROTOCOL_VERSIONS,
TRACEPARENT_META_KEY,
TRACESTATE_META_KEY
} from '../../types/constants.js';
// Enums

View file

@ -37,6 +37,44 @@ export const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapab
*/
export const LOG_LEVEL_META_KEY = 'io.modelcontextprotocol/logLevel';
/*
* Reserved `_meta` keys for distributed trace context propagation (SEP-414).
*
* These unprefixed keys are reserved by the MCP specification as an explicit
* exception to the `_meta` key prefix rule. The SDK does not interpret them;
* they pass through `_meta` untouched for OpenTelemetry-style propagation.
*/
/**
* `_meta` key carrying W3C Trace Context for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C `traceparent` header format,
* e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`.
*
* @see https://www.w3.org/TR/trace-context/#traceparent-header
*/
export const TRACEPARENT_META_KEY = 'traceparent';
/**
* `_meta` key carrying vendor-specific trace state for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C `tracestate` header format,
* e.g. `vendor1=value1,vendor2=value2`.
*
* @see https://www.w3.org/TR/trace-context/#tracestate-header
*/
export const TRACESTATE_META_KEY = 'tracestate';
/**
* `_meta` key carrying cross-cutting propagation values for distributed tracing (SEP-414).
*
* When present, the value MUST follow the W3C Baggage header format,
* e.g. `userId=alice,serverRegion=us-east-1`.
*
* @see https://www.w3.org/TR/baggage/
*/
export const BAGGAGE_META_KEY = 'baggage';
/* JSON-RPC types */
export const JSONRPC_VERSION = '2.0';

View file

@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest';
import { z } from 'zod/v4';
import { Protocol } from '../../src/shared/protocol.js';
import type { BaseContext } from '../../src/exports/public/index.js';
import { BAGGAGE_META_KEY, TRACEPARENT_META_KEY, TRACESTATE_META_KEY } from '../../src/exports/public/index.js';
import { InMemoryTransport } from '../../src/util/inMemory.js';
class TestProtocol extends Protocol<BaseContext> {
protected buildContext(ctx: BaseContext): BaseContext {
return ctx;
}
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
}
async function pair(): Promise<[TestProtocol, TestProtocol]> {
const [t1, t2] = InMemoryTransport.createLinkedPair();
const a = new TestProtocol();
const b = new TestProtocol();
await a.connect(t1);
await b.connect(t2);
return [a, b];
}
const TRACEPARENT = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const TRACESTATE = 'vendor1=opaqueValue1,vendor2=opaqueValue2';
const BAGGAGE = 'userId=alice,serverRegion=us-east-1';
describe('SEP-414 trace context `_meta` passthrough', () => {
it('exposes reserved unprefixed key names', () => {
// SEP-414 reserves these exact unprefixed keys as an exception to the
// `_meta` prefix rule; a drifted constant would break interop.
expect(TRACEPARENT_META_KEY).toBe('traceparent');
expect(TRACESTATE_META_KEY).toBe('tracestate');
expect(BAGGAGE_META_KEY).toBe('baggage');
});
it('passes request `_meta` trace context through to the server-side handler untouched', async () => {
const [a, b] = await pair();
let seenMeta: Record<string, unknown> | undefined;
b.setRequestHandler('acme/traced', { params: z.object({ v: z.string() }) }, async (params, ctx) => {
seenMeta = ctx.mcpReq._meta;
return { echoed: params.v };
});
await a.request(
{
method: 'acme/traced',
params: {
v: 'x',
_meta: {
[TRACEPARENT_META_KEY]: TRACEPARENT,
[TRACESTATE_META_KEY]: TRACESTATE,
[BAGGAGE_META_KEY]: BAGGAGE
}
}
},
z.object({ echoed: z.string() })
);
expect(seenMeta).toMatchObject({
[TRACEPARENT_META_KEY]: TRACEPARENT,
[TRACESTATE_META_KEY]: TRACESTATE,
[BAGGAGE_META_KEY]: BAGGAGE
});
});
it('passes response `_meta` trace context back to the requester untouched', async () => {
const [a, b] = await pair();
b.setRequestHandler('acme/traced', { params: z.object({}) }, async (_params, ctx) => ({
ok: true,
_meta: {
// Echo the inbound trace context onto the response envelope.
...ctx.mcpReq._meta
}
}));
const result = await a.request(
{
method: 'acme/traced',
params: {
_meta: {
[TRACEPARENT_META_KEY]: TRACEPARENT,
[TRACESTATE_META_KEY]: TRACESTATE,
[BAGGAGE_META_KEY]: BAGGAGE
}
}
},
z.object({ ok: z.boolean(), _meta: z.record(z.string(), z.unknown()).optional() })
);
expect(result.ok).toBe(true);
expect(result._meta).toMatchObject({
[TRACEPARENT_META_KEY]: TRACEPARENT,
[TRACESTATE_META_KEY]: TRACESTATE,
[BAGGAGE_META_KEY]: BAGGAGE
});
});
it('passes notification `_meta` trace context through to the handler', async () => {
const [a, b] = await pair();
let seenMeta: unknown;
b.setNotificationHandler('acme/tracedEvent', { params: z.object({ stage: z.string() }) }, (_params, notification) => {
seenMeta = notification.params?._meta;
});
await a.notification({
method: 'acme/tracedEvent',
params: {
stage: 'fetch',
_meta: { [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE }
}
});
await new Promise(r => setTimeout(r, 0));
expect(seenMeta).toEqual({ [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE });
});
});