Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sep-414-trace-context-meta-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/core': patch
---

Add reserved trace context `_meta` key constants (`TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`) per SEP-414, plus docs and a passthrough regression test. The spec reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` (W3C Trace Context / W3C Baggage formats) for distributed tracing; the SDK passes them through untouched.
56 changes: 55 additions & 1 deletion docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import {
SdkError,
SdkErrorCode,
SSEClientTransport,
StreamableHTTPClientTransport
StreamableHTTPClientTransport,
TRACEPARENT_META_KEY,
TRACESTATE_META_KEY
} from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
```
Expand Down Expand Up @@ -580,6 +582,58 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
});
```

## Trace context propagation

The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through both directions untouched — so you can propagate OpenTelemetry context across any transport, including stdio where HTTP headers are unavailable. The key names are exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.

Attach trace context to a single request via `_meta`:

```ts source="../examples/client/src/clientGuide.examples.ts#traceContext_perRequest"
// Values would normally come from your tracer's active span context.
const result = await client.callTool({
name: 'calculate-bmi',
arguments: { weightKg: 70, heightM: 1.75 },
_meta: {
[TRACEPARENT_META_KEY]: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
[TRACESTATE_META_KEY]: 'vendor1=opaqueValue1'
}
});
console.log(result.content);
```

Or inject it into every outgoing request with fetch middleware (Streamable HTTP transport):

```ts source="../examples/client/src/clientGuide.examples.ts#traceContext_middleware"
const traceContextMiddleware = createMiddleware(async (next, input, init) => {
if (typeof init?.body !== 'string') {
return next(input, init);
}
const message = JSON.parse(init.body) as {
method?: string;
params?: { _meta?: Record<string, unknown>; [key: string]: unknown };
};
// Only requests and notifications carry params._meta; skip responses.
if (message.method === undefined) {
return next(input, init);
}
message.params = {
...message.params,
_meta: {
...message.params?._meta,
// Replace with values from your tracer's active span context.
[TRACEPARENT_META_KEY]: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
}
};
return next(input, { ...init, body: JSON.stringify(message) });
});

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
fetch: applyMiddlewares(traceContextMiddleware)(fetch)
});
```

On the server side, handlers can read the incoming trace context from `ctx.mcpReq._meta` — see the [server guide](./server.md#trace-context-propagation).

## Resumption tokens

When using SSE-based streaming, the server can assign event IDs. Pass `onresumptiontoken` to track them, and `resumptionToken` to resume from where you left off after a disconnection:
Expand Down
30 changes: 29 additions & 1 deletion docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ResourceLink } from '@modelcontextprotocol/server';
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
import { completable, McpServer, ResourceTemplate, TRACEPARENT_META_KEY } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
```
Expand Down Expand Up @@ -381,6 +381,34 @@ server.registerTool(

`progress` must increase on each call. `total` and `message` are optional. If the client does not provide a `progressToken`, skip the notification.

## Trace context propagation

The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through untouched on any transport, including stdio. The key names are exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.

Read the caller's trace context from `ctx.mcpReq._meta` in a handler:

```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_traceContext"
server.registerTool(
'traced-operation',
{
description: 'Operation that participates in distributed tracing',
inputSchema: z.object({ query: z.string() })
},
async ({ query }, ctx): Promise<CallToolResult> => {
// e.g. '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
const traceparent = ctx.mcpReq._meta?.[TRACEPARENT_META_KEY];
if (typeof traceparent === 'string') {
// Continue the caller's trace, e.g. start a child span with your
// OpenTelemetry tracer using this trace context.
}

return { content: [{ type: 'text', text: `Results for ${query}` }] };
}
);
```

To propagate context onward (for example on a server-initiated sampling request, or back on a response), set the same keys in the outgoing `_meta`. See the [client guide](./client.md#trace-context-propagation) for injecting trace context on the client side.

## Server-initiated requests

MCP is bidirectional — servers can send requests *to* the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview).
Expand Down
55 changes: 54 additions & 1 deletion examples/client/src/clientGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
SdkError,
SdkErrorCode,
SSEClientTransport,
StreamableHTTPClientTransport
StreamableHTTPClientTransport,
TRACEPARENT_META_KEY,
TRACESTATE_META_KEY
} from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
//#endregion imports
Expand Down Expand Up @@ -522,6 +524,55 @@ async function middleware_basic() {
return transport;
}

/** Example: Attach W3C Trace Context to a single request via `_meta`. */
async function traceContext_perRequest(client: Client) {
//#region traceContext_perRequest
// Values would normally come from your tracer's active span context.
const result = await client.callTool({
name: 'calculate-bmi',
arguments: { weightKg: 70, heightM: 1.75 },
_meta: {
[TRACEPARENT_META_KEY]: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
[TRACESTATE_META_KEY]: 'vendor1=opaqueValue1'
}
});
console.log(result.content);
//#endregion traceContext_perRequest
}

/** Example: Client middleware that injects trace context into every outgoing request. */
async function traceContext_middleware() {
//#region traceContext_middleware
const traceContextMiddleware = createMiddleware(async (next, input, init) => {
if (typeof init?.body !== 'string') {
return next(input, init);
}
const message = JSON.parse(init.body) as {
method?: string;
params?: { _meta?: Record<string, unknown>; [key: string]: unknown };
};
// Only requests and notifications carry params._meta; skip responses.
if (message.method === undefined) {
return next(input, init);
}
message.params = {
...message.params,
_meta: {
...message.params?._meta,
// Replace with values from your tracer's active span context.
[TRACEPARENT_META_KEY]: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
}
};
return next(input, { ...init, body: JSON.stringify(message) });
});

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
fetch: applyMiddlewares(traceContextMiddleware)(fetch)
});
//#endregion traceContext_middleware
return transport;
}

/** Example: Track resumption tokens for SSE reconnection. */
async function resumptionToken_basic(client: Client) {
//#region resumptionToken_basic
Expand Down Expand Up @@ -572,4 +623,6 @@ void errorHandling_toolErrors;
void errorHandling_lifecycle;
void errorHandling_timeout;
void middleware_basic;
void traceContext_perRequest;
void traceContext_middleware;
void resumptionToken_basic;
26 changes: 25 additions & 1 deletion examples/server/src/serverGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ResourceLink } from '@modelcontextprotocol/server';
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
import { completable, McpServer, ResourceTemplate, TRACEPARENT_META_KEY } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
//#endregion imports
Expand Down Expand Up @@ -319,6 +319,29 @@ function registerTool_progress(server: McpServer) {
//#endregion registerTool_progress
}

/** Example: Tool that reads W3C Trace Context from request `_meta`. */
function registerTool_traceContext(server: McpServer) {
//#region registerTool_traceContext
server.registerTool(
'traced-operation',
{
description: 'Operation that participates in distributed tracing',
inputSchema: z.object({ query: z.string() })
},
async ({ query }, ctx): Promise<CallToolResult> => {
// e.g. '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
const traceparent = ctx.mcpReq._meta?.[TRACEPARENT_META_KEY];
if (typeof traceparent === 'string') {
// Continue the caller's trace, e.g. start a child span with your
// OpenTelemetry tracer using this trace context.
}

return { content: [{ type: 'text', text: `Results for ${query}` }] };
}
);
//#endregion registerTool_traceContext
}

// ---------------------------------------------------------------------------
// Server-initiated requests
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -543,6 +566,7 @@ void registerTool_errorHandling;
void registerTool_annotations;
void registerTool_logging;
void registerTool_progress;
void registerTool_traceContext;
void registerTool_sampling;
void registerTool_elicitation;
void registerTool_roots;
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/types/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was initially confused why these keys don't have a io.modelcontextprotocol/ prefix like the other _META_KEY constants here but that seems to be the whole point of this particular SEP.

OpenTelemetry here is an explicit exception: https://modelcontextprotocol.io/specification/draft/basic#general-fields


/**
* `_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';

Expand Down
Loading
Loading