Skip to content

Commit d5fcdc3

Browse files
committed
fix: create a map for each transport to identify different sessions to assign context through request cycle (transport to handlers)
1 parent ec5d736 commit d5fcdc3

File tree

3 files changed

+55
-69
lines changed

3 files changed

+55
-69
lines changed

packages/core/src/integrations/mcp-server/correlation.ts

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,46 @@
44
*/
55

66
import { getClient } from '../../currentScopes';
7-
import { SPAN_STATUS_ERROR, withActiveSpan } from '../../tracing';
7+
import { SPAN_STATUS_ERROR } from '../../tracing';
88
import type { Span } from '../../types-hoist/span';
99
import { extractToolResultAttributes } from './attributeExtraction';
1010
import { filterMcpPiiFromSpanData } from './piiFiltering';
11-
import type { RequestId, RequestSpanMapValue } from './types';
11+
import type { MCPTransport, RequestId, RequestSpanMapValue } from './types';
1212

13-
// Simplified correlation system that works with or without sessionId
14-
// Maps requestId directly to span data for stateless operation
15-
const requestIdToSpanMap = new Map<RequestId, RequestSpanMapValue>();
13+
// Transport-scoped correlation system that prevents collisions between different MCP sessions
14+
// Each transport instance gets its own correlation map, eliminating request ID conflicts
15+
const transportToSpanMap = new WeakMap<MCPTransport, Map<RequestId, RequestSpanMapValue>>();
16+
17+
/**
18+
* Gets or creates the span map for a specific transport instance
19+
*/
20+
function getOrCreateSpanMap(transport: MCPTransport): Map<RequestId, RequestSpanMapValue> {
21+
let spanMap = transportToSpanMap.get(transport);
22+
if (!spanMap) {
23+
spanMap = new Map();
24+
transportToSpanMap.set(transport, spanMap);
25+
}
26+
return spanMap;
27+
}
1628

1729
/**
1830
* Stores span context for later correlation with handler execution
1931
*/
20-
export function storeSpanForRequest(requestId: RequestId, span: Span, method: string): void {
21-
requestIdToSpanMap.set(requestId, {
32+
export function storeSpanForRequest(transport: MCPTransport, requestId: RequestId, span: Span, method: string): void {
33+
const spanMap = getOrCreateSpanMap(transport);
34+
spanMap.set(requestId, {
2235
span,
2336
method,
2437
startTime: Date.now(),
2538
});
2639
}
2740

28-
/**
29-
* Associates handler execution with the corresponding request span
30-
*/
31-
export function associateContextWithRequestSpan<T>(
32-
extraHandlerData: { requestId: RequestId } | undefined,
33-
cb: () => T,
34-
): T {
35-
if (extraHandlerData) {
36-
const { requestId } = extraHandlerData;
37-
38-
const spanData = requestIdToSpanMap.get(requestId);
39-
if (!spanData) {
40-
return cb();
41-
}
42-
43-
// Keep span in map for response enrichment (don't delete yet)
44-
return withActiveSpan(spanData.span, () => {
45-
return cb();
46-
});
47-
}
48-
49-
return cb();
50-
}
51-
5241
/**
5342
* Completes span with tool results and cleans up correlation
5443
*/
55-
export function completeSpanWithResults(requestId: RequestId, result: unknown): void {
56-
const spanData = requestIdToSpanMap.get(requestId);
44+
export function completeSpanWithResults(transport: MCPTransport, requestId: RequestId, result: unknown): void {
45+
const spanMap = getOrCreateSpanMap(transport);
46+
const spanData = spanMap.get(requestId);
5747
if (spanData) {
5848
const { span, method } = spanData;
5949

@@ -68,24 +58,27 @@ export function completeSpanWithResults(requestId: RequestId, result: unknown):
6858
}
6959

7060
span.end();
71-
requestIdToSpanMap.delete(requestId);
61+
spanMap.delete(requestId);
7262
}
7363
}
7464

7565
/**
76-
* Cleans up all pending spans (for transport close)
66+
* Cleans up pending spans for a specific transport (when that transport closes)
7767
*/
78-
export function cleanupAllPendingSpans(): number {
79-
const pendingCount = requestIdToSpanMap.size;
68+
export function cleanupPendingSpansForTransport(transport: MCPTransport): number {
69+
const spanMap = transportToSpanMap.get(transport);
70+
if (!spanMap) return 0;
71+
72+
const pendingCount = spanMap.size;
8073

81-
for (const [, spanData] of requestIdToSpanMap) {
74+
for (const [, spanData] of spanMap) {
8275
spanData.span.setStatus({
8376
code: SPAN_STATUS_ERROR,
8477
message: 'cancelled',
8578
});
8679
spanData.span.end();
8780
}
8881

89-
requestIdToSpanMap.clear();
82+
spanMap.clear();
9083
return pendingCount;
9184
}

packages/core/src/integrations/mcp-server/handlers.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/**
2-
* Handler wrapping functions for MCP server methods
3-
* Provides span correlation for tool, resource, and prompt handlers
2+
* Handler method wrapping for MCP server instrumentation
3+
* Provides automatic error capture and span correlation for tool, resource, and prompt handlers
44
*/
55

66
import { DEBUG_BUILD } from '../../debug-build';
77
import { debug } from '../../utils/debug-logger';
88
import { fill } from '../../utils/object';
9-
import { associateContextWithRequestSpan } from './correlation';
109
import { captureError } from './errorCapture';
1110
import type { HandlerExtraData, MCPHandler, MCPServerInstance } from './types';
1211

@@ -36,25 +35,23 @@ function createWrappedHandler(originalHandler: MCPHandler, methodName: keyof MCP
3635
try {
3736
const extraHandlerData = findExtraHandlerData(handlerArgs);
3837

39-
return associateContextWithRequestSpan(extraHandlerData, () => {
40-
return createErrorCapturingHandler.call(
41-
this,
42-
originalHandler,
43-
methodName,
44-
handlerName,
45-
handlerArgs,
46-
extraHandlerData,
47-
);
48-
});
49-
} catch (error) {
50-
DEBUG_BUILD && debug.warn('MCP handler wrapping failed:', error);
51-
return originalHandler.apply(this, handlerArgs);
52-
}
38+
return createErrorCapturingHandler.call(
39+
this,
40+
originalHandler,
41+
methodName,
42+
handlerName,
43+
handlerArgs,
44+
extraHandlerData,
45+
);
46+
} catch (error) {
47+
DEBUG_BUILD && debug.warn('MCP handler wrapping failed:', error);
48+
return originalHandler.apply(this, handlerArgs);
49+
}
5350
};
5451
}
5552

5653
/**
57-
* Creates a handler that captures execution errors for Sentry
54+
* Creates an error-capturing wrapper for handler execution
5855
*/
5956
function createErrorCapturingHandler(
6057
this: MCPServerInstance,
@@ -67,19 +64,15 @@ function createErrorCapturingHandler(
6764
try {
6865
const result = originalHandler.apply(this, handlerArgs);
6966

70-
// Handle both sync and async handlers
71-
if (result && typeof result === 'object' && 'then' in result) {
72-
// Async handler - wrap with error capture
73-
return (result as Promise<unknown>).catch((error: Error) => {
67+
if (result && typeof result === 'object' && typeof (result as { then?: unknown }).then === 'function') {
68+
return Promise.resolve(result).catch(error => {
7469
captureHandlerError(error, methodName, handlerName, handlerArgs, extraHandlerData);
7570
throw error; // Re-throw to maintain MCP error handling behavior
7671
});
7772
}
7873

79-
// Sync handler - return result as-is
8074
return result;
8175
} catch (error) {
82-
// Sync handler threw an error - capture it
8376
captureHandlerError(error as Error, methodName, handlerName, handlerArgs, extraHandlerData);
8477
throw error; // Re-throw to maintain MCP error handling behavior
8578
}

packages/core/src/integrations/mcp-server/transport.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { getIsolationScope, withIsolationScope } from '../../currentScopes';
77
import { startInactiveSpan, withActiveSpan } from '../../tracing';
88
import { fill } from '../../utils/object';
9-
import { cleanupAllPendingSpans, completeSpanWithResults, storeSpanForRequest } from './correlation';
9+
import { cleanupPendingSpansForTransport, completeSpanWithResults, storeSpanForRequest } from './correlation';
1010
import { captureError } from './errorCapture';
1111
import { buildMcpServerSpanConfig, createMcpNotificationSpan, createMcpOutgoingNotificationSpan } from './spans';
1212
import type { ExtraHandlerData, MCPTransport } from './types';
@@ -30,8 +30,7 @@ export function wrapTransportOnMessage(transport: MCPTransport): void {
3030
const spanConfig = buildMcpServerSpanConfig(jsonRpcMessage, this, extra as ExtraHandlerData);
3131
const span = startInactiveSpan(spanConfig);
3232

33-
// Store span context for handler correlation using requestId
34-
storeSpanForRequest(messageTyped.id, span, messageTyped.method);
33+
storeSpanForRequest(this, messageTyped.id, span, messageTyped.method);
3534

3635
// Execute handler within span context
3736
return withActiveSpan(span, () => {
@@ -74,7 +73,7 @@ export function wrapTransportSend(transport: MCPTransport): void {
7473
captureJsonRpcErrorResponse(messageTyped.error, messageTyped.id, this);
7574
}
7675

77-
completeSpanWithResults(messageTyped.id, messageTyped.result);
76+
completeSpanWithResults(this, messageTyped.id, messageTyped.result);
7877
}
7978
}
8079

@@ -85,13 +84,14 @@ export function wrapTransportSend(transport: MCPTransport): void {
8584
}
8685

8786
/**
88-
* Wraps transport.onclose to clean up pending spans
87+
* Wraps transport.onclose to clean up pending spans for this transport only
8988
*/
9089
export function wrapTransportOnClose(transport: MCPTransport): void {
9190
if (transport.onclose) {
9291
fill(transport, 'onclose', originalOnClose => {
9392
return function (this: MCPTransport, ...args: unknown[]) {
94-
cleanupAllPendingSpans();
93+
// Clean up only spans associated with this specific transport
94+
cleanupPendingSpansForTransport(this);
9595

9696
return (originalOnClose as (...args: unknown[]) => unknown).call(this, ...args);
9797
};

0 commit comments

Comments
 (0)