Skip to content

Commit 1efda74

Browse files
committed
(draft) fix span duration
1 parent 97990c6 commit 1efda74

File tree

3 files changed

+161
-13
lines changed

3 files changed

+161
-13
lines changed

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

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { fill } from '../../utils/object';
2-
import type { ExtraHandlerData, MCPServerInstance, MCPTransport } from './types';
2+
import { logger } from '../../utils/logger';
3+
import { DEBUG_BUILD } from '../../debug-build';
4+
import type { ExtraHandlerData, MCPServerInstance, MCPTransport, McpHandlerExtra } from './types';
35
import {
46
createMcpNotificationSpan,
57
createMcpOutgoingNotificationSpan,
@@ -10,6 +12,18 @@ import {
1012
} from './utils';
1113

1214
const wrappedMcpServerInstances = new WeakSet();
15+
const wrappedHandlerMethods = new WeakSet();
16+
17+
// Map to track handler completion promises by request ID
18+
const requestToHandlerPromiseMap = new Map<string | number, {
19+
resolve: (value: unknown) => void;
20+
reject: (reason: unknown) => void;
21+
}>();
22+
23+
/**
24+
* Type for MCP handler callbacks
25+
*/
26+
type McpHandlerCallback = (...args: unknown[]) => unknown | Promise<unknown>;
1327

1428
/**
1529
* Wraps a MCP Server instance from the `@modelcontextprotocol/sdk` package with Sentry instrumentation.
@@ -28,24 +42,44 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
2842

2943
const serverInstance = mcpServerInstance as MCPServerInstance;
3044

45+
// Wrap tool, resource, and prompt methods to ensure proper async context
46+
wrapHandlerMethods(serverInstance);
47+
3148
fill(serverInstance, 'connect', (originalConnect) => {
3249
return async function(this: MCPServerInstance, transport: MCPTransport, ...restArgs: unknown[]) {
3350
const result = await originalConnect.call(this, transport, ...restArgs);
3451

3552
if (transport.onmessage) {
3653
fill(transport, 'onmessage', (originalOnMessage) => {
37-
return function(this: MCPTransport, jsonRpcMessage: unknown, extra?: unknown) {
54+
return async function(this: MCPTransport, jsonRpcMessage: unknown, extra?: unknown) {
3855
if (isJsonRpcRequest(jsonRpcMessage)) {
39-
return createMcpServerSpan(jsonRpcMessage, this, extra as ExtraHandlerData, () => {
40-
return originalOnMessage.call(this, jsonRpcMessage, extra);
56+
return await createMcpServerSpan(jsonRpcMessage, this, extra as ExtraHandlerData, async () => {
57+
const request = jsonRpcMessage as { id: string | number; method: string };
58+
59+
const handlerPromise = new Promise<unknown>((resolve, reject) => {
60+
requestToHandlerPromiseMap.set(request.id, { resolve, reject });
61+
62+
setTimeout(() => {
63+
const entry = requestToHandlerPromiseMap.get(request.id);
64+
if (entry) {
65+
requestToHandlerPromiseMap.delete(request.id);
66+
resolve(undefined);
67+
}
68+
}, 30000);
69+
});
70+
71+
const originalResult = originalOnMessage.call(this, jsonRpcMessage, extra);
72+
await handlerPromise;
73+
return originalResult;
4174
});
4275
}
76+
4377
if (isJsonRpcNotification(jsonRpcMessage)) {
44-
return createMcpNotificationSpan(jsonRpcMessage, this, extra as ExtraHandlerData, () => {
45-
return originalOnMessage.call(this, jsonRpcMessage, extra);
78+
return await createMcpNotificationSpan(jsonRpcMessage, this, extra as ExtraHandlerData, async () => {
79+
return await originalOnMessage.call(this, jsonRpcMessage, extra);
4680
});
4781
}
48-
return originalOnMessage.call(this, jsonRpcMessage, extra);
82+
return await originalOnMessage.call(this, jsonRpcMessage, extra);
4983
};
5084
});
5185
}
@@ -54,18 +88,23 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
5488
fill(transport, 'send', (originalSend) => {
5589
return async function(this: MCPTransport, message: unknown) {
5690
if (isJsonRpcNotification(message)) {
57-
return createMcpOutgoingNotificationSpan(message, this, () => {
58-
return originalSend.call(this, message);
91+
return await createMcpOutgoingNotificationSpan(message, this, async () => {
92+
return await originalSend.call(this, message);
5993
});
6094
}
61-
return originalSend.call(this, message);
95+
return await originalSend.call(this, message);
6296
};
6397
});
6498
}
6599

66100
if (transport.onclose) {
67101
fill(transport, 'onclose', (originalOnClose) => {
68102
return function(this: MCPTransport, ...args: unknown[]) {
103+
for (const [, promiseEntry] of requestToHandlerPromiseMap.entries()) {
104+
promiseEntry.resolve(undefined);
105+
}
106+
requestToHandlerPromiseMap.clear();
107+
69108
return originalOnClose.call(this, ...args);
70109
};
71110
});
@@ -78,6 +117,98 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
78117
return mcpServerInstance as S;
79118
}
80119

120+
/**
121+
* Wraps the tool, resource, and prompt registration methods to ensure
122+
* handlers execute within the correct span context
123+
*/
124+
function wrapHandlerMethods(serverInstance: MCPServerInstance): void {
125+
if (wrappedHandlerMethods.has(serverInstance)) {
126+
return;
127+
}
128+
129+
fill(serverInstance, 'tool', (originalTool) => {
130+
return function(this: MCPServerInstance, ...args: unknown[]) {
131+
const toolName = args[0] as string;
132+
const lastArg = args[args.length - 1];
133+
134+
if (typeof lastArg !== 'function') {
135+
return originalTool.apply(this, args);
136+
}
137+
138+
const wrappedCallback = wrapHandlerCallback(lastArg as McpHandlerCallback, 'tool', toolName);
139+
const newArgs = [...args.slice(0, -1), wrappedCallback];
140+
141+
return originalTool.apply(this, newArgs);
142+
};
143+
});
144+
145+
fill(serverInstance, 'resource', (originalResource) => {
146+
return function(this: MCPServerInstance, ...args: unknown[]) {
147+
const resourceName = args[0] as string;
148+
const lastArg = args[args.length - 1];
149+
150+
if (typeof lastArg !== 'function') {
151+
return originalResource.apply(this, args);
152+
}
153+
154+
const wrappedCallback = wrapHandlerCallback(lastArg as McpHandlerCallback, 'resource', resourceName);
155+
const newArgs = [...args.slice(0, -1), wrappedCallback];
156+
157+
return originalResource.apply(this, newArgs);
158+
};
159+
});
160+
161+
fill(serverInstance, 'prompt', (originalPrompt) => {
162+
return function(this: MCPServerInstance, ...args: unknown[]) {
163+
const promptName = args[0] as string;
164+
const lastArg = args[args.length - 1];
165+
166+
if (typeof lastArg !== 'function') {
167+
return originalPrompt.apply(this, args);
168+
}
169+
170+
const wrappedCallback = wrapHandlerCallback(lastArg as McpHandlerCallback, 'prompt', promptName);
171+
const newArgs = [...args.slice(0, -1), wrappedCallback];
172+
173+
return originalPrompt.apply(this, newArgs);
174+
};
175+
});
176+
177+
wrappedHandlerMethods.add(serverInstance);
178+
}
179+
180+
/**
181+
* Wraps a handler callback to ensure it executes within the correct span context
182+
*/
183+
function wrapHandlerCallback(callback: McpHandlerCallback, handlerType: string, handlerName: string): McpHandlerCallback {
184+
return async function(this: unknown, ...args: unknown[]) {
185+
const extra = args.find((arg): arg is McpHandlerExtra =>
186+
typeof arg === 'object' &&
187+
arg !== null &&
188+
'requestId' in arg
189+
);
190+
191+
if (extra?.requestId) {
192+
const promiseEntry = requestToHandlerPromiseMap.get(extra.requestId);
193+
194+
if (promiseEntry) {
195+
try {
196+
const result = await callback.apply(this, args);
197+
requestToHandlerPromiseMap.delete(extra.requestId);
198+
promiseEntry.resolve(result);
199+
return result;
200+
} catch (error) {
201+
requestToHandlerPromiseMap.delete(extra.requestId);
202+
promiseEntry.reject(error);
203+
throw error;
204+
}
205+
}
206+
}
207+
208+
return await callback.apply(this, args);
209+
};
210+
}
211+
81212
// =============================================================================
82213
// SESSION AND REQUEST CORRELATION (Legacy support)
83214
// =============================================================================

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ export interface McpSpanConfig {
132132
callback: () => unknown;
133133
}
134134

135+
/**
136+
* Type for the extra parameter passed to MCP handlers
137+
*/
138+
export interface McpHandlerExtra {
139+
sessionId?: string;
140+
requestId: string | number;
141+
[key: string]: unknown;
142+
}
143+
135144

136145
export type SessionId = string;
137146
export type RequestId = string | number;

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
* Essential utility functions for MCP server instrumentation
33
*/
44

5+
import { startSpan } from '../../tracing';
6+
import { logger } from '../../utils/logger';
57
import { DEBUG_BUILD } from '../../debug-build';
68
import {
79
SEMANTIC_ATTRIBUTE_SENTRY_OP,
810
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
911
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1012
} from '../../semanticAttributes';
11-
import { startSpan } from '../../tracing';
12-
import { logger } from '../../utils/logger';
1313
import {
1414
CLIENT_ADDRESS_ATTRIBUTE,
1515
CLIENT_PORT_ATTRIBUTE,
@@ -29,7 +29,14 @@ import {
2929
NETWORK_PROTOCOL_VERSION_ATTRIBUTE,
3030
NETWORK_TRANSPORT_ATTRIBUTE,
3131
} from './attributes';
32-
import type { ExtraHandlerData, JsonRpcNotification, JsonRpcRequest, McpSpanConfig, MCPTransport, MethodConfig } from './types';
32+
import type {
33+
ExtraHandlerData,
34+
JsonRpcNotification,
35+
JsonRpcRequest,
36+
MCPTransport,
37+
McpSpanConfig,
38+
MethodConfig,
39+
} from './types';
3340
import { isURLObjectRelative, parseStringToURLObject } from '../../utils/url';
3441

3542
/** Validates if a message is a JSON-RPC request */
@@ -292,6 +299,7 @@ function createMcpSpan(config: McpSpanConfig): unknown {
292299
...buildSentryAttributes(type),
293300
};
294301

302+
// Use startSpan with manual control to ensure proper async handling
295303
return startSpan(
296304
{
297305
name: spanName,

0 commit comments

Comments
 (0)