@@ -4,8 +4,17 @@ import {
44 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
55 SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
66} from './semanticAttributes' ;
7- import { startSpan } from './tracing' ;
7+ import { startSpan , withActiveSpan } from './tracing' ;
8+ import type { Span } from './types-hoist/span' ;
89import { logger } from './utils-hoist/logger' ;
10+ import { getActiveSpan } from './utils/spanUtils' ;
11+
12+ interface MCPTransport {
13+ // The first argument is a JSON RPC message
14+ onmessage ?: ( ...args : unknown [ ] ) => void ;
15+ onclose ?: ( ...args : unknown [ ] ) => void ;
16+ sessionId ?: string ;
17+ }
918
1019interface MCPServerInstance {
1120 // The first arg is always a name, the last arg should always be a callback function (ie a handler).
@@ -15,6 +24,7 @@ interface MCPServerInstance {
1524 tool : ( name : string , ...args : unknown [ ] ) => void ;
1625 // The first arg is always a name, the last arg should always be a callback function (ie a handler).
1726 prompt : ( name : string , ...args : unknown [ ] ) => void ;
27+ connect ( transport : MCPTransport ) : Promise < void > ;
1828}
1929
2030const wrappedMcpServerInstances = new WeakSet ( ) ;
@@ -35,6 +45,59 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
3545 return mcpServerInstance ;
3646 }
3747
48+ // eslint-disable-next-line @typescript-eslint/unbound-method
49+ mcpServerInstance . connect = new Proxy ( mcpServerInstance . connect , {
50+ apply ( target , thisArg , argArray ) {
51+ const [ transport , ...restArgs ] = argArray as [ MCPTransport , ...unknown [ ] ] ;
52+
53+ if ( ! transport . onclose ) {
54+ transport . onclose = ( ) => {
55+ if ( transport . sessionId ) {
56+ handleTransportOnClose ( transport . sessionId ) ;
57+ }
58+ } ;
59+ }
60+
61+ if ( ! transport . onmessage ) {
62+ transport . onmessage = jsonRpcMessage => {
63+ if ( transport . sessionId && isJsonRPCMessageWithRequestId ( jsonRpcMessage ) ) {
64+ handleTransportOnMessage ( transport . sessionId , jsonRpcMessage . id ) ;
65+ }
66+ } ;
67+ }
68+
69+ const patchedTransport = new Proxy ( transport , {
70+ set ( target , key , value ) {
71+ if ( key === 'onmessage' ) {
72+ target [ key ] = new Proxy ( value , {
73+ apply ( onMessageTarget , onMessageThisArg , onMessageArgArray ) {
74+ const [ jsonRpcMessage ] = onMessageArgArray ;
75+ if ( transport . sessionId && isJsonRPCMessageWithRequestId ( jsonRpcMessage ) ) {
76+ handleTransportOnMessage ( transport . sessionId , jsonRpcMessage . id ) ;
77+ }
78+ return Reflect . apply ( onMessageTarget , onMessageThisArg , onMessageArgArray ) ;
79+ } ,
80+ } ) ;
81+ } else if ( key === 'onclose' ) {
82+ target [ key ] = new Proxy ( value , {
83+ apply ( onCloseTarget , onCloseThisArg , onCloseArgArray ) {
84+ if ( transport . sessionId ) {
85+ handleTransportOnClose ( transport . sessionId ) ;
86+ }
87+ return Reflect . apply ( onCloseTarget , onCloseThisArg , onCloseArgArray ) ;
88+ } ,
89+ } ) ;
90+ } else {
91+ target [ key as keyof MCPTransport ] = value ;
92+ }
93+ return true ;
94+ } ,
95+ } ) ;
96+
97+ return Reflect . apply ( target , thisArg , [ patchedTransport , ...restArgs ] ) ;
98+ } ,
99+ } ) ;
100+
38101 mcpServerInstance . resource = new Proxy ( mcpServerInstance . resource , {
39102 apply ( target , thisArg , argArray ) {
40103 const resourceName : unknown = argArray [ 0 ] ;
@@ -44,19 +107,28 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
44107 return target . apply ( thisArg , argArray ) ;
45108 }
46109
47- return startSpan (
48- {
49- name : `mcp-server/resource:${ resourceName } ` ,
50- forceTransaction : true ,
51- attributes : {
52- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
53- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
54- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
55- 'mcp_server.resource' : resourceName ,
56- } ,
110+ const wrappedResourceHandler = new Proxy ( resourceHandler , {
111+ apply ( resourceHandlerTarget , resourceHandlerThisArg , resourceHandlerArgArray ) {
112+ const extraHandlerDataWithRequestId = resourceHandlerArgArray . find ( isExtraHandlerDataWithRequestId ) ;
113+ return associateContextWithRequestSpan ( extraHandlerDataWithRequestId , ( ) => {
114+ return startSpan (
115+ {
116+ name : `mcp-server/resource:${ resourceName } ` ,
117+ forceTransaction : true ,
118+ attributes : {
119+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
120+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
121+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
122+ 'mcp_server.resource' : resourceName ,
123+ } ,
124+ } ,
125+ ( ) => resourceHandlerTarget . apply ( resourceHandlerThisArg , resourceHandlerArgArray ) ,
126+ ) ;
127+ } ) ;
57128 } ,
58- ( ) => target . apply ( thisArg , argArray ) ,
59- ) ;
129+ } ) ;
130+
131+ return Reflect . apply ( target , thisArg , [ ...argArray . slice ( 0 , - 1 ) , wrappedResourceHandler ] ) ;
60132 } ,
61133 } ) ;
62134
@@ -69,19 +141,28 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
69141 return target . apply ( thisArg , argArray ) ;
70142 }
71143
72- return startSpan (
73- {
74- name : `mcp-server/tool:${ toolName } ` ,
75- forceTransaction : true ,
76- attributes : {
77- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
78- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
79- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
80- 'mcp_server.tool' : toolName ,
81- } ,
144+ const wrappedToolHandler = new Proxy ( toolHandler , {
145+ apply ( toolHandlerTarget , toolHandlerThisArg , toolHandlerArgArray ) {
146+ const extraHandlerDataWithRequestId = toolHandlerArgArray . find ( isExtraHandlerDataWithRequestId ) ;
147+ return associateContextWithRequestSpan ( extraHandlerDataWithRequestId , ( ) => {
148+ return startSpan (
149+ {
150+ name : `mcp-server/tool:${ toolName } ` ,
151+ forceTransaction : true ,
152+ attributes : {
153+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
154+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
155+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
156+ 'mcp_server.tool' : toolName ,
157+ } ,
158+ } ,
159+ ( ) => toolHandlerTarget . apply ( toolHandlerThisArg , toolHandlerArgArray ) ,
160+ ) ;
161+ } ) ;
82162 } ,
83- ( ) => target . apply ( thisArg , argArray ) ,
84- ) ;
163+ } ) ;
164+
165+ return Reflect . apply ( target , thisArg , [ ...argArray . slice ( 0 , - 1 ) , wrappedToolHandler ] ) ;
85166 } ,
86167 } ) ;
87168
@@ -94,19 +175,28 @@ export function wrapMcpServerWithSentry<S extends object>(mcpServerInstance: S):
94175 return target . apply ( thisArg , argArray ) ;
95176 }
96177
97- return startSpan (
98- {
99- name : `mcp-server/resource:${ promptName } ` ,
100- forceTransaction : true ,
101- attributes : {
102- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
103- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
104- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
105- 'mcp_server.prompt' : promptName ,
106- } ,
178+ const wrappedPromptHandler = new Proxy ( promptHandler , {
179+ apply ( promptHandlerTarget , promptHandlerThisArg , promptHandlerArgArray ) {
180+ const extraHandlerDataWithRequestId = promptHandlerArgArray . find ( isExtraHandlerDataWithRequestId ) ;
181+ return associateContextWithRequestSpan ( extraHandlerDataWithRequestId , ( ) => {
182+ return startSpan (
183+ {
184+ name : `mcp-server/prompt:${ promptName } ` ,
185+ forceTransaction : true ,
186+ attributes : {
187+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'auto.function.mcp-server' ,
188+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.function.mcp-server' ,
189+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
190+ 'mcp_server.prompt' : promptName ,
191+ } ,
192+ } ,
193+ ( ) => promptHandlerTarget . apply ( promptHandlerThisArg , promptHandlerArgArray ) ,
194+ ) ;
195+ } ) ;
107196 } ,
108- ( ) => target . apply ( thisArg , argArray ) ,
109- ) ;
197+ } ) ;
198+
199+ return Reflect . apply ( target , thisArg , [ ...argArray . slice ( 0 , - 1 ) , wrappedPromptHandler ] ) ;
110200 } ,
111201 } ) ;
112202
@@ -124,6 +214,79 @@ function isMcpServerInstance(mcpServerInstance: unknown): mcpServerInstance is M
124214 'tool' in mcpServerInstance &&
125215 typeof mcpServerInstance . tool === 'function' &&
126216 'prompt' in mcpServerInstance &&
127- typeof mcpServerInstance . prompt === 'function'
217+ typeof mcpServerInstance . prompt === 'function' &&
218+ 'connect' in mcpServerInstance &&
219+ typeof mcpServerInstance . connect === 'function'
220+ ) ;
221+ }
222+
223+ function isJsonRPCMessageWithRequestId ( target : unknown ) : target is { id : RequestId } {
224+ return (
225+ typeof target === 'object' &&
226+ target !== null &&
227+ 'id' in target &&
228+ ( typeof target . id === 'number' || typeof target . id === 'string' )
128229 ) ;
129230}
231+
232+ interface ExtraHandlerDataWithRequestId {
233+ sessionId : SessionId ;
234+ requestId : RequestId ;
235+ }
236+
237+ // Note that not all versions of the MCP library have `requestId` as a field on the extra data.
238+ function isExtraHandlerDataWithRequestId ( target : unknown ) : target is ExtraHandlerDataWithRequestId {
239+ return (
240+ typeof target === 'object' &&
241+ target !== null &&
242+ 'sessionId' in target &&
243+ typeof target . sessionId === 'string' &&
244+ 'requestId' in target &&
245+ ( typeof target . requestId === 'number' || typeof target . requestId === 'string' )
246+ ) ;
247+ }
248+
249+ type SessionId = string ;
250+ type RequestId = string | number ;
251+
252+ const sessionAndRequestToRequestParentSpanMap = new Map < SessionId , Map < RequestId , Span > > ( ) ;
253+
254+ function handleTransportOnClose ( sessionId : SessionId ) : void {
255+ sessionAndRequestToRequestParentSpanMap . delete ( sessionId ) ;
256+ }
257+
258+ function handleTransportOnMessage ( sessionId : SessionId , requestId : RequestId ) : void {
259+ const activeSpan = getActiveSpan ( ) ;
260+ if ( activeSpan ) {
261+ const requestIdToSpanMap = sessionAndRequestToRequestParentSpanMap . get ( sessionId ) ?? new Map ( ) ;
262+ requestIdToSpanMap . set ( requestId , activeSpan ) ;
263+ sessionAndRequestToRequestParentSpanMap . set ( sessionId , requestIdToSpanMap ) ;
264+ }
265+ }
266+
267+ function associateContextWithRequestSpan < T > (
268+ extraHandlerData : ExtraHandlerDataWithRequestId | undefined ,
269+ cb : ( ) => T ,
270+ ) : T {
271+ if ( extraHandlerData ) {
272+ const { sessionId, requestId } = extraHandlerData ;
273+ const requestIdSpanMap = sessionAndRequestToRequestParentSpanMap . get ( sessionId ) ;
274+
275+ if ( ! requestIdSpanMap ) {
276+ return cb ( ) ;
277+ }
278+
279+ const span = requestIdSpanMap . get ( requestId ) ;
280+ if ( ! span ) {
281+ return cb ( ) ;
282+ }
283+
284+ // remove the span from the map so it can be garbage collected
285+ requestIdSpanMap . delete ( requestId ) ;
286+ return withActiveSpan ( span , ( ) => {
287+ return cb ( ) ;
288+ } ) ;
289+ }
290+
291+ return cb ( ) ;
292+ }
0 commit comments