Skip to content

Commit e818fcb

Browse files
logaretmclaude
andauthored
fix(core): handle stateless MCP wrapper transport correlation (#20293)
This PR handles stateless MCP wrappers by using the connected transport instance as the stable MCP correlation key across wrapper layers. I verified the issue and the fix by adding a regression test for stateless wrapper transports where the request path runs on the inner transport and the response path runs on the wrapper. Closes #20290 --------- Co-authored-by: GPT-5 <noreply@anthropic.com>
1 parent 0ffc262 commit e818fcb

File tree

2 files changed

+98
-9
lines changed

2 files changed

+98
-9
lines changed

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
4343
if (isInitialize) {
4444
try {
4545
initSessionData = extractSessionDataFromInitializeRequest(message);
46-
storeSessionDataForTransport(this, initSessionData);
46+
storeSessionDataForTransport(transport, initSessionData);
4747
} catch {
4848
// noop
4949
}
@@ -52,7 +52,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
5252
const isolationScope = getIsolationScope().clone();
5353

5454
return withIsolationScope(isolationScope, () => {
55-
const spanConfig = buildMcpServerSpanConfig(message, this, extra as ExtraHandlerData, options);
55+
const spanConfig = buildMcpServerSpanConfig(message, transport, extra as ExtraHandlerData, options);
5656
const span = startInactiveSpan(spanConfig);
5757

5858
// For initialize requests, add client info directly to span (works even for stateless transports)
@@ -65,7 +65,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
6565
});
6666
}
6767

68-
storeSpanForRequest(this, message.id, span, message.method);
68+
storeSpanForRequest(transport, message.id, span, message.method);
6969

7070
return withActiveSpan(span, () => {
7171
return (originalOnMessage as (...args: unknown[]) => unknown).call(this, message, extra);
@@ -74,7 +74,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
7474
}
7575

7676
if (isJsonRpcNotification(message)) {
77-
return createMcpNotificationSpan(message, this, extra as ExtraHandlerData, options, () => {
77+
return createMcpNotificationSpan(message, transport, extra as ExtraHandlerData, options, () => {
7878
return (originalOnMessage as (...args: unknown[]) => unknown).call(this, message, extra);
7979
});
8080
}
@@ -99,7 +99,7 @@ export function wrapTransportSend(transport: MCPTransport, options: ResolvedMcpO
9999
const [message] = args;
100100

101101
if (isJsonRpcNotification(message)) {
102-
return createMcpOutgoingNotificationSpan(message, this, options, () => {
102+
return createMcpOutgoingNotificationSpan(message, transport, options, () => {
103103
return (originalSend as (...args: unknown[]) => unknown).call(this, ...args);
104104
});
105105
}
@@ -114,14 +114,14 @@ export function wrapTransportSend(transport: MCPTransport, options: ResolvedMcpO
114114
if (message.result.protocolVersion || message.result.serverInfo) {
115115
try {
116116
const serverData = extractSessionDataFromInitializeResponse(message.result);
117-
updateSessionDataForTransport(this, serverData);
117+
updateSessionDataForTransport(transport, serverData);
118118
} catch {
119119
// noop
120120
}
121121
}
122122
}
123123

124-
completeSpanWithResults(this, message.id, message.result, options, !!message.error);
124+
completeSpanWithResults(transport, message.id, message.result, options, !!message.error);
125125
}
126126
}
127127

@@ -139,8 +139,8 @@ export function wrapTransportOnClose(transport: MCPTransport): void {
139139
if (transport.onclose) {
140140
fill(transport, 'onclose', originalOnClose => {
141141
return function (this: MCPTransport, ...args: unknown[]) {
142-
cleanupPendingSpansForTransport(this);
143-
cleanupSessionDataForTransport(this);
142+
cleanupPendingSpansForTransport(transport);
143+
cleanupSessionDataForTransport(transport);
144144
return (originalOnClose as (...args: unknown[]) => unknown).call(this, ...args);
145145
};
146146
});

packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,95 @@ describe('MCP Server Transport Instrumentation', () => {
880880
expect(mockSpan.end).toHaveBeenCalled();
881881
});
882882

883+
it('should correlate spans correctly for stateless wrapper transports', async () => {
884+
const { wrapper, inner } = createMockWrapperTransport('stateless-wrapper-session');
885+
inner.sessionId = undefined;
886+
887+
const mockMcpServer = createMockMcpServer();
888+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
889+
890+
await wrappedMcpServer.connect(wrapper);
891+
892+
const mockSpan = { setAttributes: vi.fn(), end: vi.fn() };
893+
startInactiveSpanSpy.mockReturnValue(mockSpan as any);
894+
895+
inner.onmessage?.call(
896+
inner,
897+
{
898+
jsonrpc: '2.0',
899+
method: 'tools/call',
900+
id: 'stateless-wrapper-req-1',
901+
params: { name: 'test-tool' },
902+
},
903+
{},
904+
);
905+
906+
await wrapper.send({
907+
jsonrpc: '2.0',
908+
id: 'stateless-wrapper-req-1',
909+
result: { content: [{ type: 'text', text: 'success' }] },
910+
});
911+
912+
expect(mockSpan.end).toHaveBeenCalled();
913+
});
914+
915+
it('should preserve session metadata for later stateless wrapper spans', async () => {
916+
const { wrapper, inner } = createMockWrapperTransport('stateless-wrapper-session');
917+
inner.sessionId = undefined;
918+
919+
const mockMcpServer = createMockMcpServer();
920+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
921+
922+
await wrappedMcpServer.connect(wrapper);
923+
924+
inner.onmessage?.call(
925+
inner,
926+
{
927+
jsonrpc: '2.0',
928+
method: 'initialize',
929+
id: 'init-stateless',
930+
params: {
931+
protocolVersion: '2025-06-18',
932+
clientInfo: { name: 'test-client', version: '1.0.0' },
933+
},
934+
},
935+
{},
936+
);
937+
938+
await wrapper.send({
939+
jsonrpc: '2.0',
940+
id: 'init-stateless',
941+
result: {
942+
protocolVersion: '2025-06-18',
943+
serverInfo: { name: 'test-server', version: '2.0.0' },
944+
capabilities: {},
945+
},
946+
});
947+
948+
inner.onmessage?.call(
949+
inner,
950+
{
951+
jsonrpc: '2.0',
952+
method: 'tools/call',
953+
id: 'stateless-wrapper-req-2',
954+
params: { name: 'test-tool' },
955+
},
956+
{},
957+
);
958+
959+
expect(startInactiveSpanSpy).toHaveBeenCalledWith(
960+
expect.objectContaining({
961+
attributes: expect.objectContaining({
962+
'mcp.client.name': 'test-client',
963+
'mcp.client.version': '1.0.0',
964+
'mcp.protocol.version': '2025-06-18',
965+
'mcp.server.name': 'test-server',
966+
'mcp.server.version': '2.0.0',
967+
}),
968+
}),
969+
);
970+
});
971+
883972
it('should handle initialize request/response with wrapper transport', async () => {
884973
const { wrapper } = createMockWrapperTransport('init-wrapper-session');
885974
const mockMcpServer = createMockMcpServer();

0 commit comments

Comments
 (0)