1+ /** 
2+  * Attribute extraction and building functions for MCP server instrumentation 
3+  */ 
4+ 
5+ import  {  isURLObjectRelative ,  parseStringToURLObject  }  from  '../../utils/url' ; 
6+ import  { 
7+   CLIENT_ADDRESS_ATTRIBUTE , 
8+   CLIENT_PORT_ATTRIBUTE , 
9+   MCP_PROMPT_NAME_ATTRIBUTE , 
10+   MCP_REQUEST_ID_ATTRIBUTE , 
11+   MCP_RESOURCE_URI_ATTRIBUTE , 
12+   MCP_SESSION_ID_ATTRIBUTE , 
13+   MCP_TOOL_NAME_ATTRIBUTE , 
14+   MCP_TRANSPORT_ATTRIBUTE , 
15+   NETWORK_PROTOCOL_VERSION_ATTRIBUTE , 
16+   NETWORK_TRANSPORT_ATTRIBUTE , 
17+ }  from  './attributes' ; 
18+ import  type  {  ExtraHandlerData ,  JsonRpcNotification ,  JsonRpcRequest ,  McpSpanType , MCPTransport ,  MethodConfig  }  from  './types' ; 
19+ 
20+ /** Configuration for MCP methods to extract targets and arguments */ 
21+ const  METHOD_CONFIGS : Record < string ,  MethodConfig >  =  { 
22+   'tools/call' : { 
23+     targetField : 'name' , 
24+     targetAttribute : MCP_TOOL_NAME_ATTRIBUTE , 
25+     captureArguments : true , 
26+     argumentsField : 'arguments' , 
27+   } , 
28+   'resources/read' : { 
29+     targetField : 'uri' , 
30+     targetAttribute : MCP_RESOURCE_URI_ATTRIBUTE , 
31+     captureUri : true , 
32+   } , 
33+   'resources/subscribe' : { 
34+     targetField : 'uri' , 
35+     targetAttribute : MCP_RESOURCE_URI_ATTRIBUTE , 
36+   } , 
37+   'resources/unsubscribe' : { 
38+     targetField : 'uri' , 
39+     targetAttribute : MCP_RESOURCE_URI_ATTRIBUTE , 
40+   } , 
41+   'prompts/get' : { 
42+     targetField : 'name' , 
43+     targetAttribute : MCP_PROMPT_NAME_ATTRIBUTE , 
44+     captureName : true , 
45+     captureArguments : true , 
46+     argumentsField : 'arguments' , 
47+   } , 
48+ } ; 
49+ 
50+ /** Extracts target info from method and params based on method type */ 
51+ export  function  extractTargetInfo ( method : string ,  params : Record < string ,  unknown > ) : { 
52+   target ?: string ; 
53+   attributes : Record < string ,  string > 
54+ }  { 
55+   const  config  =  METHOD_CONFIGS [ method  as  keyof  typeof  METHOD_CONFIGS ] ; 
56+   if  ( ! config )  { 
57+     return  {  attributes : { }  } ; 
58+   } 
59+ 
60+   const  target  =  config . targetField  &&  typeof  params ?. [ config . targetField ]  ===  'string' 
61+     ? params [ config . targetField ]  as  string 
62+     : undefined ; 
63+ 
64+   return  { 
65+     target, 
66+     attributes : target  &&  config . targetAttribute  ? {  [ config . targetAttribute ] : target  }  : { } 
67+   } ; 
68+ } 
69+ 
70+ /** Extracts request arguments based on method type */ 
71+ export  function  getRequestArguments ( method : string ,  params : Record < string ,  unknown > ) : Record < string ,  string >  { 
72+   const  args : Record < string ,  string >  =  { } ; 
73+   const  config  =  METHOD_CONFIGS [ method  as  keyof  typeof  METHOD_CONFIGS ] ; 
74+ 
75+   if  ( ! config )  { 
76+     return  args ; 
77+   } 
78+ 
79+   // Capture arguments from the configured field 
80+   if  ( config . captureArguments  &&  config . argumentsField  &&  params ?. [ config . argumentsField ] )  { 
81+     const  argumentsObj  =  params [ config . argumentsField ] ; 
82+     if  ( typeof  argumentsObj  ===  'object'  &&  argumentsObj  !==  null )  { 
83+       for  ( const  [ key ,  value ]  of  Object . entries ( argumentsObj  as  Record < string ,  unknown > ) )  { 
84+         args [ `mcp.request.argument.${ key . toLowerCase ( ) }  ` ]  =  JSON . stringify ( value ) ; 
85+       } 
86+     } 
87+   } 
88+ 
89+   // Capture specific fields as arguments 
90+   if  ( config . captureUri  &&  params ?. uri )  { 
91+     args [ 'mcp.request.argument.uri' ]  =  JSON . stringify ( params . uri ) ; 
92+   } 
93+ 
94+   if  ( config . captureName  &&  params ?. name )  { 
95+     args [ 'mcp.request.argument.name' ]  =  JSON . stringify ( params . name ) ; 
96+   } 
97+ 
98+   return  args ; 
99+ } 
100+ 
101+ /** Extracts transport types based on transport constructor name */ 
102+ export  function  getTransportTypes ( transport : MCPTransport ) : {  mcpTransport : string ;  networkTransport : string  }  { 
103+   const  transportName  =  transport . constructor ?. name ?. toLowerCase ( )  ||  '' ; 
104+ 
105+   // Standard MCP transports per specification 
106+   if  ( transportName . includes ( 'stdio' ) )  { 
107+     return  {  mcpTransport : 'stdio' ,  networkTransport : 'pipe'  } ; 
108+   } 
109+ 
110+   // Streamable HTTP is the standard HTTP-based transport 
111+   if  ( transportName . includes ( 'streamablehttp' )  ||  transportName . includes ( 'streamable' ) )  { 
112+     return  {  mcpTransport : 'http' ,  networkTransport : 'tcp'  } ; 
113+   } 
114+ 
115+   // SSE is deprecated (backwards compatibility) 
116+   if  ( transportName . includes ( 'sse' ) )  { 
117+     return  {  mcpTransport : 'sse' ,  networkTransport : 'tcp'  } ; 
118+   } 
119+ 
120+   // For custom transports, mark as unknown 
121+   return  {  mcpTransport : 'unknown' ,  networkTransport : 'unknown'  } ; 
122+ } 
123+ 
124+ /** Extracts additional attributes for specific notification types */ 
125+ export  function  getNotificationAttributes ( 
126+   method : string , 
127+   params : Record < string ,  unknown > , 
128+ ) : Record < string ,  string  |  number >  { 
129+   const  attributes : Record < string ,  string  |  number >  =  { } ; 
130+ 
131+   switch  ( method )  { 
132+     case  'notifications/cancelled' :
133+       if  ( params ?. requestId )  { 
134+         attributes [ 'mcp.cancelled.request_id' ]  =  String ( params . requestId ) ; 
135+       } 
136+       if  ( params ?. reason )  { 
137+         attributes [ 'mcp.cancelled.reason' ]  =  String ( params . reason ) ; 
138+       } 
139+       break ; 
140+ 
141+     case  'notifications/message' :
142+       if  ( params ?. level )  { 
143+         attributes [ 'mcp.logging.level' ]  =  String ( params . level ) ; 
144+       } 
145+       if  ( params ?. logger )  { 
146+         attributes [ 'mcp.logging.logger' ]  =  String ( params . logger ) ; 
147+       } 
148+       if  ( params ?. data  !==  undefined )  { 
149+         attributes [ 'mcp.logging.data_type' ]  =  typeof  params . data ; 
150+         // Store the actual message content 
151+         if  ( typeof  params . data  ===  'string' )  { 
152+           attributes [ 'mcp.logging.message' ]  =  params . data ; 
153+         }  else  { 
154+           attributes [ 'mcp.logging.message' ]  =  JSON . stringify ( params . data ) ; 
155+         } 
156+       } 
157+       break ; 
158+ 
159+     case  'notifications/progress' :
160+       if  ( params ?. progressToken )  { 
161+         attributes [ 'mcp.progress.token' ]  =  String ( params . progressToken ) ; 
162+       } 
163+       if  ( typeof  params ?. progress  ===  'number' )  { 
164+         attributes [ 'mcp.progress.current' ]  =  params . progress ; 
165+       } 
166+       if  ( typeof  params ?. total  ===  'number' )  { 
167+         attributes [ 'mcp.progress.total' ]  =  params . total ; 
168+         if  ( typeof  params ?. progress  ===  'number' )  { 
169+           attributes [ 'mcp.progress.percentage' ]  =  ( params . progress  /  params . total )  *  100 ; 
170+         } 
171+       } 
172+       if  ( params ?. message )  { 
173+         attributes [ 'mcp.progress.message' ]  =  String ( params . message ) ; 
174+       } 
175+       break ; 
176+ 
177+     case  'notifications/resources/updated' :
178+       if  ( params ?. uri )  { 
179+         attributes [ 'mcp.resource.uri' ]  =  String ( params . uri ) ; 
180+         // Extract protocol from URI 
181+         const  urlObject  =  parseStringToURLObject ( String ( params . uri ) ) ; 
182+         if  ( urlObject  &&  ! isURLObjectRelative ( urlObject ) )  { 
183+           attributes [ 'mcp.resource.protocol' ]  =  urlObject . protocol . replace ( ':' ,  '' ) ; 
184+         } 
185+       } 
186+       break ; 
187+ 
188+     case  'notifications/initialized' :
189+       attributes [ 'mcp.lifecycle.phase' ]  =  'initialization_complete' ; 
190+       attributes [ 'mcp.protocol.ready' ]  =  1 ; 
191+       break ; 
192+   } 
193+ 
194+   return  attributes ; 
195+ } 
196+ 
197+ /** Extracts client connection info from extra handler data */ 
198+ export  function  extractClientInfo ( extra : ExtraHandlerData ) : { 
199+   address ?: string ; 
200+   port ?: number 
201+ }  { 
202+   return  { 
203+     address : extra ?. requestInfo ?. remoteAddress  || 
204+              extra ?. clientAddress  || 
205+              extra ?. request ?. ip  || 
206+              extra ?. request ?. connection ?. remoteAddress , 
207+     port : extra ?. requestInfo ?. remotePort  || 
208+           extra ?. clientPort  || 
209+           extra ?. request ?. connection ?. remotePort 
210+   } ; 
211+ } 
212+ 
213+ /** Build transport and network attributes */ 
214+ export  function  buildTransportAttributes ( 
215+   transport : MCPTransport , 
216+   extra ?: ExtraHandlerData , 
217+ ) : Record < string ,  string  |  number >  { 
218+   const  sessionId  =  transport . sessionId ; 
219+   const  clientInfo  =  extra  ? extractClientInfo ( extra )  : { } ; 
220+   const  {  mcpTransport,  networkTransport }  =  getTransportTypes ( transport ) ; 
221+ 
222+   return  { 
223+     ...( sessionId  &&  {  [ MCP_SESSION_ID_ATTRIBUTE ] : sessionId  } ) , 
224+     ...( clientInfo . address  &&  {  [ CLIENT_ADDRESS_ATTRIBUTE ] : clientInfo . address  } ) , 
225+     ...( clientInfo . port  &&  {  [ CLIENT_PORT_ATTRIBUTE ] : clientInfo . port  } ) , 
226+     [ MCP_TRANSPORT_ATTRIBUTE ] : mcpTransport , 
227+     [ NETWORK_TRANSPORT_ATTRIBUTE ] : networkTransport , 
228+     [ NETWORK_PROTOCOL_VERSION_ATTRIBUTE ] : '2.0' , 
229+   } ; 
230+ } 
231+ 
232+ /** Build type-specific attributes based on message type */ 
233+ export  function  buildTypeSpecificAttributes ( 
234+   type : McpSpanType , 
235+   message : JsonRpcRequest  |  JsonRpcNotification , 
236+   params ?: Record < string ,  unknown > , 
237+ ) : Record < string ,  string  |  number >  { 
238+   if  ( type  ===  'request' )  { 
239+     const  request  =  message  as  JsonRpcRequest ; 
240+     const  targetInfo  =  extractTargetInfo ( request . method ,  params  ||  { } ) ; 
241+ 
242+     return  { 
243+       ...( request . id  !==  undefined  &&  {  [ MCP_REQUEST_ID_ATTRIBUTE ] : String ( request . id )  } ) , 
244+       ...targetInfo . attributes , 
245+       ...getRequestArguments ( request . method ,  params  ||  { } ) , 
246+     } ; 
247+   } 
248+ 
249+   // For notifications, only include notification-specific attributes 
250+   return  getNotificationAttributes ( message . method ,  params  ||  { } ) ; 
251+ } 
252+ 
253+ /** Simplified tool result attribute extraction */ 
254+ export  function  extractSimpleToolAttributes ( result : unknown ) : Record < string ,  string  |  number  |  boolean >  { 
255+   const  attributes : Record < string ,  string  |  number  |  boolean >  =  { } ; 
256+   
257+   if  ( typeof  result  ===  'object'  &&  result  !==  null )  { 
258+     const  resultObj  =  result  as  Record < string ,  unknown > ; 
259+     
260+     // Check if this is an error result 
261+     if  ( typeof  resultObj . isError  ===  'boolean' )  { 
262+       attributes [ 'mcp.tool.result.is_error' ]  =  resultObj . isError ; 
263+     } 
264+     
265+     // Extract basic content info 
266+     if  ( Array . isArray ( resultObj . content ) )  { 
267+       attributes [ 'mcp.tool.result.content_count' ]  =  resultObj . content . length ; 
268+       
269+       // Extract info from all content items 
270+       for  ( let  i  =  0 ;  i  <  resultObj . content . length ;  i ++ )  { 
271+         const  item  =  resultObj . content [ i ] ; 
272+         if  ( item  &&  typeof  item  ===  'object'  &&  item  !==  null )  { 
273+           const  contentItem  =  item  as  Record < string ,  unknown > ; 
274+           const  prefix  =  resultObj . content . length  ===  1  ? 'mcp.tool.result'  : `mcp.tool.result.${ i }  ` ; 
275+           
276+           // Always capture the content type 
277+           if  ( typeof  contentItem . type  ===  'string' )  { 
278+             attributes [ `${ prefix }  .content_type` ]  =  contentItem . type ; 
279+           } 
280+           
281+           // Extract common fields generically 
282+           if  ( typeof  contentItem . text  ===  'string' )  { 
283+             const  text  =  contentItem . text ; 
284+             attributes [ `${ prefix }  .content` ]  =  text . length  >  500  ? `${ text . substring ( 0 ,  497 ) }  ...`  : text ; 
285+           } 
286+           
287+           if  ( typeof  contentItem . mimeType  ===  'string' )  { 
288+             attributes [ `${ prefix }  .mime_type` ]  =  contentItem . mimeType ; 
289+           } 
290+           
291+           if  ( typeof  contentItem . uri  ===  'string' )  { 
292+             attributes [ `${ prefix }  .uri` ]  =  contentItem . uri ; 
293+           } 
294+           
295+           if  ( typeof  contentItem . name  ===  'string' )  { 
296+             attributes [ `${ prefix }  .name` ]  =  contentItem . name ; 
297+           } 
298+           
299+           if  ( typeof  contentItem . data  ===  'string' )  { 
300+             attributes [ `${ prefix }  .data_size` ]  =  contentItem . data . length ; 
301+           } 
302+           
303+           // For embedded resources, check the nested resource object 
304+           if  ( contentItem . resource  &&  typeof  contentItem . resource  ===  'object' )  { 
305+             const  resource  =  contentItem . resource  as  Record < string ,  unknown > ; 
306+             if  ( typeof  resource . uri  ===  'string' )  { 
307+               attributes [ `${ prefix }  .resource_uri` ]  =  resource . uri ; 
308+             } 
309+             if  ( typeof  resource . mimeType  ===  'string' )  { 
310+               attributes [ `${ prefix }  .resource_mime_type` ]  =  resource . mimeType ; 
311+             } 
312+           } 
313+         } 
314+       } 
315+     } 
316+   } 
317+   
318+   return  attributes ; 
319+ } 
0 commit comments