Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -571,6 +573,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 @@ -378,6 +378,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';

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