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