1
1
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' ;
3
5
import {
4
6
createMcpNotificationSpan ,
5
7
createMcpOutgoingNotificationSpan ,
@@ -10,6 +12,18 @@ import {
10
12
} from './utils' ;
11
13
12
14
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 > ;
13
27
14
28
/**
15
29
* 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):
28
42
29
43
const serverInstance = mcpServerInstance as MCPServerInstance ;
30
44
45
+ // Wrap tool, resource, and prompt methods to ensure proper async context
46
+ wrapHandlerMethods ( serverInstance ) ;
47
+
31
48
fill ( serverInstance , 'connect' , ( originalConnect ) => {
32
49
return async function ( this : MCPServerInstance , transport : MCPTransport , ...restArgs : unknown [ ] ) {
33
50
const result = await originalConnect . call ( this , transport , ...restArgs ) ;
34
51
35
52
if ( transport . onmessage ) {
36
53
fill ( transport , 'onmessage' , ( originalOnMessage ) => {
37
- return function ( this : MCPTransport , jsonRpcMessage : unknown , extra ?: unknown ) {
54
+ return async function ( this : MCPTransport , jsonRpcMessage : unknown , extra ?: unknown ) {
38
55
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 ;
41
74
} ) ;
42
75
}
76
+
43
77
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 ) ;
46
80
} ) ;
47
81
}
48
- return originalOnMessage . call ( this , jsonRpcMessage , extra ) ;
82
+ return await originalOnMessage . call ( this , jsonRpcMessage , extra ) ;
49
83
} ;
50
84
} ) ;
51
85
}
@@ -54,18 +88,23 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
54
88
fill ( transport , 'send' , ( originalSend ) => {
55
89
return async function ( this : MCPTransport , message : unknown ) {
56
90
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 ) ;
59
93
} ) ;
60
94
}
61
- return originalSend . call ( this , message ) ;
95
+ return await originalSend . call ( this , message ) ;
62
96
} ;
63
97
} ) ;
64
98
}
65
99
66
100
if ( transport . onclose ) {
67
101
fill ( transport , 'onclose' , ( originalOnClose ) => {
68
102
return function ( this : MCPTransport , ...args : unknown [ ] ) {
103
+ for ( const [ , promiseEntry ] of requestToHandlerPromiseMap . entries ( ) ) {
104
+ promiseEntry . resolve ( undefined ) ;
105
+ }
106
+ requestToHandlerPromiseMap . clear ( ) ;
107
+
69
108
return originalOnClose . call ( this , ...args ) ;
70
109
} ;
71
110
} ) ;
@@ -78,6 +117,98 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
78
117
return mcpServerInstance as S ;
79
118
}
80
119
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
+
81
212
// =============================================================================
82
213
// SESSION AND REQUEST CORRELATION (Legacy support)
83
214
// =============================================================================
0 commit comments