33// ---------------------------------------------------------------------------
44
55import { DurableObject , env } from "cloudflare:workers" ;
6+ import { createTraceState } from "@opentelemetry/api" ;
67import { Data , Effect , Layer } from "effect" ;
78import * as OtelTracer from "@effect/opentelemetry/Tracer" ;
89import * as Sentry from "@sentry/cloudflare" ;
@@ -36,6 +37,12 @@ export type McpSessionInit = {
3637 organizationId : string ;
3738} ;
3839
40+ export type IncomingTraceHeaders = {
41+ readonly traceparent ?: string ;
42+ readonly tracestate ?: string ;
43+ readonly baggage ?: string ;
44+ } ;
45+
3946const HEARTBEAT_MS = 30 * 1000 ;
4047const SESSION_TIMEOUT_MS = 5 * 60 * 1000 ;
4148const TRANSPORT_STATE_KEY = "transport" ;
@@ -59,31 +66,41 @@ const jsonRpcError = (status: number, code: number, message: string) =>
5966 headers : { "content-type" : "application/json" } ,
6067 } ) ;
6168
62- // W3C traceparent propagation across the worker→DO boundary. mcp.ts injects
63- // the worker-side `mcp.request` SpanContext as a `traceparent` header on
64- // forwarded requests (and as a second arg to `init()`). We parse it here and
65- // use `OtelTracer.withSpanContext` to stitch the DO's root span under the
66- // worker span so the entire logical request lives in one Axiom trace.
69+ // W3C propagation across the worker→DO boundary. mcp.ts injects the worker's
70+ // `traceparent` and forwards incoming `tracestate` / `baggage` headers on
71+ // forwarded requests (and as a second arg to `init()`). We parse the context
72+ // here and use `OtelTracer.withSpanContext` to stitch the DO's root span
73+ // under the worker span so the entire logical request lives in one trace.
6774const TRACEPARENT_PATTERN = / ^ ( [ 0 - 9 a - f ] { 2 } ) - ( [ 0 - 9 a - f ] { 32 } ) - ( [ 0 - 9 a - f ] { 16 } ) - ( [ 0 - 9 a - f ] { 2 } ) $ / ;
6875
6976type IncomingSpanContext = {
7077 readonly traceId : string ;
7178 readonly spanId : string ;
7279 readonly traceFlags : number ;
80+ readonly traceState ?: ReturnType < typeof createTraceState > ;
7381} ;
7482
75- const parseTraceparent = ( value : string | null | undefined ) : IncomingSpanContext | null => {
83+ const parseTraceparent = (
84+ traceparent : string | null | undefined ,
85+ tracestate : string | null | undefined ,
86+ ) : IncomingSpanContext | null => {
87+ const value = traceparent ;
7688 if ( ! value ) return null ;
7789 const match = TRACEPARENT_PATTERN . exec ( value ) ;
7890 if ( ! match ) return null ;
79- return { traceId : match [ 2 ] ! , spanId : match [ 3 ] ! , traceFlags : parseInt ( match [ 4 ] ! , 16 ) } ;
91+ return {
92+ traceId : match [ 2 ] ! ,
93+ spanId : match [ 3 ] ! ,
94+ traceFlags : parseInt ( match [ 4 ] ! , 16 ) ,
95+ ...( tracestate ? { traceState : createTraceState ( tracestate ) } : { } ) ,
96+ } ;
8097} ;
8198
8299const withIncomingParent = < A , E , R > (
83- traceparent : string | null | undefined ,
100+ incoming : IncomingTraceHeaders | null | undefined ,
84101 effect : Effect . Effect < A , E , R > ,
85102) : Effect . Effect < A , E , R > => {
86- const parsed = parseTraceparent ( traceparent ) ;
103+ const parsed = parseTraceparent ( incoming ?. traceparent , incoming ?. tracestate ) ;
87104 return parsed ? OtelTracer . withSpanContext ( effect , parsed ) : effect ;
88105} ;
89106
@@ -250,14 +267,14 @@ export class McpSessionDO extends DurableObject {
250267 } ) ;
251268 }
252269
253- async init ( token : McpSessionInit , traceparent ?: string ) : Promise < void > {
270+ async init ( token : McpSessionInit , incoming ?: IncomingTraceHeaders ) : Promise < void > {
254271 if ( this . initialized ) return ;
255272 return Effect . runPromise (
256273 this . doInitEffect ( token ) . pipe (
257274 Effect . withSpan ( "McpSessionDO.init" , {
258275 attributes : { "mcp.auth.organization_id" : token . organizationId } ,
259276 } ) ,
260- ( eff ) => withIncomingParent ( traceparent , eff ) ,
277+ ( eff ) => withIncomingParent ( incoming , eff ) ,
261278 Effect . provide ( DoTelemetryLive ) ,
262279 ) ,
263280 ) ;
@@ -355,7 +372,11 @@ export class McpSessionDO extends DurableObject {
355372 // only (method, session-id presence, response status); rich client
356373 // fingerprint stays on the edge `mcp.request` span, which shares a
357374 // trace_id with this one.
358- const traceparent = request . headers . get ( "traceparent" ) ;
375+ const incoming = {
376+ traceparent : request . headers . get ( "traceparent" ) ?? undefined ,
377+ tracestate : request . headers . get ( "tracestate" ) ?? undefined ,
378+ baggage : request . headers . get ( "baggage" ) ?? undefined ,
379+ } satisfies IncomingTraceHeaders ;
359380 const program = Effect . promise ( ( ) => this . dispatchRequest ( request ) ) . pipe (
360381 Effect . tap ( ( response ) =>
361382 Effect . annotateCurrentSpan ( {
@@ -368,7 +389,7 @@ export class McpSessionDO extends DurableObject {
368389 "mcp.request.session_id_present" : ! ! request . headers . get ( "mcp-session-id" ) ,
369390 } ,
370391 } ) ,
371- ( eff ) => withIncomingParent ( traceparent , eff ) ,
392+ ( eff ) => withIncomingParent ( incoming , eff ) ,
372393 Effect . provide ( DoTelemetryLive ) ,
373394 ) ;
374395 return Effect . runPromise ( program ) ;
0 commit comments