1
- import {
2
- SEMANTIC_ATTRIBUTE_SENTRY_OP ,
3
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
4
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
5
- } from './semanticAttributes' ;
6
- import { startSpan , withActiveSpan } from './tracing' ;
7
1
import type { Span } from './types-hoist/span' ;
8
- import { getActiveSpan } from './utils/spanUtils' ;
9
- import {
10
- MCP_METHOD_NAME_ATTRIBUTE ,
11
- MCP_REQUEST_ID_ATTRIBUTE ,
12
- MCP_SESSION_ID_ATTRIBUTE ,
13
- MCP_TRANSPORT_ATTRIBUTE ,
14
- NETWORK_TRANSPORT_ATTRIBUTE ,
15
- NETWORK_PROTOCOL_VERSION_ATTRIBUTE ,
16
- CLIENT_ADDRESS_ATTRIBUTE ,
17
- CLIENT_PORT_ATTRIBUTE ,
18
- MCP_NOTIFICATION_DIRECTION_ATTRIBUTE ,
19
- MCP_SERVER_OP_VALUE ,
20
- MCP_FUNCTION_ORIGIN_VALUE ,
21
- MCP_NOTIFICATION_ORIGIN_VALUE ,
22
- MCP_ROUTE_SOURCE_VALUE ,
23
- } from './utils/mcp-server/attributes' ;
24
2
import type {
25
- JsonRpcRequest ,
26
- JsonRpcNotification ,
27
- MCPTransport ,
3
+ ExtraHandlerData ,
28
4
MCPServerInstance ,
5
+ MCPTransport ,
29
6
SessionId ,
30
- RequestId ,
31
- ExtraHandlerData ,
32
7
} from './utils/mcp-server/types' ;
33
8
import {
34
- isJsonRpcRequest ,
9
+ createMcpNotificationSpan ,
10
+ createMcpOutgoingNotificationSpan ,
11
+ createMcpServerSpan ,
35
12
isJsonRpcNotification ,
36
- extractTarget ,
37
- getTargetAttributes ,
38
- getRequestArguments ,
39
- getTransportTypes ,
40
- getNotificationDescription ,
41
- getNotificationAttributes ,
42
- extractClientAddress ,
43
- extractClientPort ,
13
+ isJsonRpcRequest ,
44
14
validateMcpServerInstance ,
45
- createSpanName ,
46
15
} from './utils/mcp-server/utils' ;
47
16
48
17
const wrappedMcpServerInstances = new WeakSet ( ) ;
@@ -65,55 +34,56 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
65
34
const serverInstance = mcpServerInstance as MCPServerInstance ;
66
35
67
36
// Wrap connect() to intercept AFTER Protocol sets up transport handlers
68
- serverInstance . connect = new Proxy ( serverInstance . connect , {
37
+ const originalConnect = serverInstance . connect ;
38
+ serverInstance . connect = new Proxy ( originalConnect , {
69
39
async apply ( target , thisArg , argArray ) {
70
40
const [ transport , ...restArgs ] = argArray as [ MCPTransport , ...unknown [ ] ] ;
71
41
72
42
// Call the original connect first to let Protocol set up its handlers
73
43
const result = await Reflect . apply ( target , thisArg , [ transport , ...restArgs ] ) ;
74
-
44
+
75
45
// Intercept incoming messages via onmessage
76
46
if ( transport . onmessage ) {
77
47
const protocolOnMessage = transport . onmessage ;
78
-
48
+
79
49
transport . onmessage = new Proxy ( protocolOnMessage , {
80
50
apply ( onMessageTarget , onMessageThisArg , onMessageArgs ) {
81
51
const [ jsonRpcMessage , extra ] = onMessageArgs ;
82
-
52
+
83
53
// Instrument both requests and notifications
84
54
if ( isJsonRpcRequest ( jsonRpcMessage ) ) {
85
55
return createMcpServerSpan ( jsonRpcMessage , transport , extra as ExtraHandlerData , ( ) => {
86
- return onMessageTarget . apply ( onMessageThisArg , onMessageArgs ) ;
56
+ return Reflect . apply ( onMessageTarget , onMessageThisArg , onMessageArgs ) ;
87
57
} ) ;
88
58
}
89
59
if ( isJsonRpcNotification ( jsonRpcMessage ) ) {
90
60
return createMcpNotificationSpan ( jsonRpcMessage , transport , extra as ExtraHandlerData , ( ) => {
91
- return onMessageTarget . apply ( onMessageThisArg , onMessageArgs ) ;
61
+ return Reflect . apply ( onMessageTarget , onMessageThisArg , onMessageArgs ) ;
92
62
} ) ;
93
63
}
94
-
95
- return onMessageTarget . apply ( onMessageThisArg , onMessageArgs ) ;
96
- }
64
+
65
+ return Reflect . apply ( onMessageTarget , onMessageThisArg , onMessageArgs ) ;
66
+ } ,
97
67
} ) ;
98
68
}
99
69
100
70
// Intercept outgoing messages via send
101
71
if ( transport . send ) {
102
72
const originalSend = transport . send ;
103
-
73
+
104
74
transport . send = new Proxy ( originalSend , {
105
75
async apply ( sendTarget , sendThisArg , sendArgs ) {
106
76
const [ message , options ] = sendArgs ;
107
-
77
+
108
78
// Instrument outgoing notifications (but not requests/responses)
109
79
if ( isJsonRpcNotification ( message ) ) {
110
80
return createMcpOutgoingNotificationSpan ( message , transport , options as Record < string , unknown > , ( ) => {
111
- return sendTarget . apply ( sendThisArg , sendArgs ) ;
81
+ return Reflect . apply ( sendTarget , sendThisArg , sendArgs ) ;
112
82
} ) ;
113
83
}
114
-
115
- return sendTarget . apply ( sendThisArg , sendArgs ) ;
116
- }
84
+
85
+ return Reflect . apply ( sendTarget , sendThisArg , sendArgs ) ;
86
+ } ,
117
87
} ) ;
118
88
}
119
89
@@ -125,8 +95,8 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
125
95
if ( transport . sessionId ) {
126
96
handleTransportOnClose ( transport . sessionId ) ;
127
97
}
128
- return onCloseTarget . apply ( onCloseThisArg , onCloseArgs ) ;
129
- }
98
+ return Reflect . apply ( onCloseTarget , onCloseThisArg , onCloseArgs ) ;
99
+ } ,
130
100
} ) ;
131
101
}
132
102
return result ;
@@ -137,203 +107,49 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
137
107
return mcpServerInstance as S ;
138
108
}
139
109
140
- function createMcpServerSpan (
141
- jsonRpcMessage : JsonRpcRequest ,
142
- transport : MCPTransport ,
143
- extra : ExtraHandlerData ,
144
- callback : ( ) => unknown
145
- ) {
146
- const { method, id : requestId , params } = jsonRpcMessage ;
147
-
148
- // Extract target from method and params for proper description
149
- const target = extractTarget ( method , params as Record < string , unknown > ) ;
150
- const description = createSpanName ( method , target ) ;
151
-
152
- // Session ID should come from the transport itself, not the RPC message
153
- const sessionId = transport . sessionId ;
154
-
155
- // Extract client information from extra/request data
156
- const clientAddress = extractClientAddress ( extra ) ;
157
- const clientPort = extractClientPort ( extra ) ;
158
-
159
- // Determine transport types
160
- const { mcpTransport, networkTransport } = getTransportTypes ( transport ) ;
161
-
162
- const attributes : Record < string , string | number > = {
163
- [ MCP_METHOD_NAME_ATTRIBUTE ] : method ,
164
-
165
- ...( requestId !== undefined && { [ MCP_REQUEST_ID_ATTRIBUTE ] : String ( requestId ) } ) ,
166
- ...( target && getTargetAttributes ( method , target ) ) ,
167
- ...( sessionId && { [ MCP_SESSION_ID_ATTRIBUTE ] : sessionId } ) ,
168
- ...( clientAddress && { [ CLIENT_ADDRESS_ATTRIBUTE ] : clientAddress } ) ,
169
- ...( clientPort && { [ CLIENT_PORT_ATTRIBUTE ] : clientPort } ) ,
170
- [ MCP_TRANSPORT_ATTRIBUTE ] : mcpTransport , // Application level: "http", "sse", "stdio", "websocket"
171
- [ NETWORK_TRANSPORT_ATTRIBUTE ] : networkTransport , // Network level: "tcp", "pipe", "udp", "quic"
172
- [ NETWORK_PROTOCOL_VERSION_ATTRIBUTE ] : '2.0' , // JSON-RPC version
173
-
174
- // Opt-in: Tool arguments (if enabled)
175
- ...getRequestArguments ( method , params as Record < string , unknown > ) ,
176
-
177
- // Sentry-specific attributes
178
- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : MCP_SERVER_OP_VALUE ,
179
- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : MCP_FUNCTION_ORIGIN_VALUE ,
180
- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : MCP_ROUTE_SOURCE_VALUE
181
- } ;
182
-
183
- return startSpan ( {
184
- name : description ,
185
- forceTransaction : true ,
186
- attributes
187
- } , ( ) => {
188
- // TODO(bete): add proper error handling. Handle JSON RPC errors in the result
189
- return callback ( ) ;
190
- } ) ;
191
- }
192
-
193
- function createMcpNotificationSpan (
194
- jsonRpcMessage : JsonRpcNotification ,
195
- transport : MCPTransport ,
196
- extra : ExtraHandlerData ,
197
- callback : ( ) => unknown
198
- ) {
199
- const { method, params } = jsonRpcMessage ;
200
-
201
- const description = getNotificationDescription ( method , params as Record < string , unknown > ) ;
202
-
203
- const sessionId = transport . sessionId ;
204
-
205
- // Extract client information
206
- const clientAddress = extractClientAddress ( extra ) ;
207
- const clientPort = extractClientPort ( extra ) ;
208
-
209
- // Determine transport types
210
- const { mcpTransport, networkTransport } = getTransportTypes ( transport ) ;
211
-
212
- const notificationAttribs = getNotificationAttributes ( method , params as Record < string , unknown > ) ;
213
-
214
- const attributes : Record < string , string | number > = {
215
- [ MCP_METHOD_NAME_ATTRIBUTE ] : method ,
216
- [ MCP_NOTIFICATION_DIRECTION_ATTRIBUTE ] : 'client_to_server' , // Incoming notification
217
-
218
- ...( sessionId && { [ MCP_SESSION_ID_ATTRIBUTE ] : sessionId } ) ,
219
- ...( clientAddress && { [ CLIENT_ADDRESS_ATTRIBUTE ] : clientAddress } ) ,
220
- ...( clientPort && { [ CLIENT_PORT_ATTRIBUTE ] : clientPort } ) ,
221
- [ MCP_TRANSPORT_ATTRIBUTE ] : mcpTransport ,
222
- [ NETWORK_TRANSPORT_ATTRIBUTE ] : networkTransport ,
223
- [ NETWORK_PROTOCOL_VERSION_ATTRIBUTE ] : '2.0' ,
224
-
225
- // Notification-specific attributes
226
- ...notificationAttribs ,
227
-
228
- // Sentry-specific attributes
229
- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : MCP_SERVER_OP_VALUE ,
230
- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : MCP_NOTIFICATION_ORIGIN_VALUE ,
231
- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : MCP_ROUTE_SOURCE_VALUE
232
- } ;
233
-
234
- return startSpan ( {
235
- name : description ,
236
- forceTransaction : true ,
237
- attributes
238
- } , ( ) => {
239
- const result = callback ( ) ;
240
- return result ;
241
- } ) ;
242
- }
243
-
244
- function createMcpOutgoingNotificationSpan (
245
- jsonRpcMessage : JsonRpcNotification ,
246
- transport : MCPTransport ,
247
- options : Record < string , unknown > ,
248
- callback : ( ) => unknown
249
- ) {
250
- const { method, params } = jsonRpcMessage ;
251
-
252
- const description = getNotificationDescription ( method , params as Record < string , unknown > ) ;
253
-
254
- const sessionId = transport . sessionId ;
255
-
256
- // Determine transport types
257
- const { mcpTransport, networkTransport } = getTransportTypes ( transport ) ;
258
-
259
- const notificationAttribs = getNotificationAttributes ( method , params as Record < string , unknown > ) ;
260
-
261
- const attributes : Record < string , string | number > = {
262
- [ MCP_METHOD_NAME_ATTRIBUTE ] : method ,
263
- [ MCP_NOTIFICATION_DIRECTION_ATTRIBUTE ] : 'server_to_client' , // Outgoing notification
264
-
265
- ...( sessionId && { [ MCP_SESSION_ID_ATTRIBUTE ] : sessionId } ) ,
266
- [ MCP_TRANSPORT_ATTRIBUTE ] : mcpTransport ,
267
- [ NETWORK_TRANSPORT_ATTRIBUTE ] : networkTransport ,
268
- [ NETWORK_PROTOCOL_VERSION_ATTRIBUTE ] : '2.0' ,
269
-
270
- // Notification-specific attributes
271
- ...notificationAttribs ,
272
-
273
- // Sentry-specific attributes
274
- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : MCP_SERVER_OP_VALUE ,
275
- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : MCP_NOTIFICATION_ORIGIN_VALUE ,
276
- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : MCP_ROUTE_SOURCE_VALUE
277
- } ;
278
-
279
- return startSpan ( {
280
- name : description ,
281
- forceTransaction : true ,
282
- attributes
283
- } , ( ) => {
284
- const result = callback ( ) ;
285
- return result ;
286
- } ) ;
287
- }
288
-
289
110
// =============================================================================
290
111
// SESSION AND REQUEST CORRELATION (Legacy support)
291
112
// =============================================================================
292
113
293
- interface ExtraHandlerDataWithRequestId {
294
- sessionId : SessionId ;
295
- requestId : RequestId ;
296
- }
297
-
298
- const sessionAndRequestToRequestParentSpanMap = new Map < SessionId , Map < RequestId , Span > > ( ) ;
114
+ const sessionAndRequestToRequestParentSpanMap = new Map < SessionId , Map < string , Span > > ( ) ;
299
115
300
116
function handleTransportOnClose ( sessionId : SessionId ) : void {
301
117
sessionAndRequestToRequestParentSpanMap . delete ( sessionId ) ;
302
118
}
303
119
304
120
// TODO(bete): refactor this and associateContextWithRequestSpan to use the new span API.
305
- function handleTransportOnMessage ( sessionId : SessionId , requestId : RequestId ) : void {
306
- const activeSpan = getActiveSpan ( ) ;
307
- if ( activeSpan ) {
308
- const requestIdToSpanMap = sessionAndRequestToRequestParentSpanMap . get ( sessionId ) ?? new Map ( ) ;
309
- requestIdToSpanMap . set ( requestId , activeSpan ) ;
310
- sessionAndRequestToRequestParentSpanMap . set ( sessionId , requestIdToSpanMap ) ;
311
- }
312
- }
313
-
314
- function associateContextWithRequestSpan < T > (
315
- extraHandlerData : ExtraHandlerDataWithRequestId | undefined ,
316
- cb : ( ) => T ,
317
- ) : T {
318
- if ( extraHandlerData ) {
319
- const { sessionId, requestId } = extraHandlerData ;
320
- const requestIdSpanMap = sessionAndRequestToRequestParentSpanMap . get ( sessionId ) ;
321
-
322
- if ( ! requestIdSpanMap ) {
323
- return cb ( ) ;
324
- }
325
-
326
- const span = requestIdSpanMap . get ( requestId ) ;
327
- if ( ! span ) {
328
- return cb ( ) ;
329
- }
330
-
331
- // remove the span from the map so it can be garbage collected
332
- requestIdSpanMap . delete ( requestId ) ;
333
- return withActiveSpan ( span , ( ) => {
334
- return cb ( ) ;
335
- } ) ;
336
- }
337
-
338
- return cb ( ) ;
339
- }
121
+ // function handleTransportOnMessage(sessionId: SessionId, requestId: string ): void {
122
+ // const activeSpan = getActiveSpan();
123
+ // if (activeSpan) {
124
+ // const requestIdToSpanMap = sessionAndRequestToRequestParentSpanMap.get(sessionId) ?? new Map();
125
+ // requestIdToSpanMap.set(requestId, activeSpan);
126
+ // sessionAndRequestToRequestParentSpanMap.set(sessionId, requestIdToSpanMap);
127
+ // }
128
+ // }
129
+
130
+ // function associateContextWithRequestSpan<T>(
131
+ // extraHandlerData: { sessionId: SessionId; requestId: string } | undefined,
132
+ // cb: () => T,
133
+ // ): T {
134
+ // if (extraHandlerData) {
135
+ // const { sessionId, requestId } = extraHandlerData;
136
+ // const requestIdSpanMap = sessionAndRequestToRequestParentSpanMap.get(sessionId);
137
+
138
+ // if (!requestIdSpanMap) {
139
+ // return cb();
140
+ // }
141
+
142
+ // const span = requestIdSpanMap.get(requestId);
143
+ // if (!span) {
144
+ // return cb();
145
+ // }
146
+
147
+ // // remove the span from the map so it can be garbage collected
148
+ // requestIdSpanMap.delete(requestId);
149
+ // return withActiveSpan(span, () => {
150
+ // return cb();
151
+ // });
152
+ // }
153
+
154
+ // return cb();
155
+ // }
0 commit comments