1
+ import { isURLObjectRelative , parseStringToURLObject } from '../../utils/url' ;
2
+ import { METHOD_CONFIGS } from './config' ;
3
+ import type { ExtraHandlerData } from './types' ;
4
+
5
+ /**
6
+ * Extracts target info from method and params based on method type
7
+ */
8
+ export function extractTargetInfo (
9
+ method : string ,
10
+ params : Record < string , unknown > ,
11
+ ) : {
12
+ target ?: string ;
13
+ attributes : Record < string , string > ;
14
+ } {
15
+ const config = METHOD_CONFIGS [ method as keyof typeof METHOD_CONFIGS ] ;
16
+ if ( ! config ) {
17
+ return { attributes : { } } ;
18
+ }
19
+
20
+ const target =
21
+ config . targetField && typeof params ?. [ config . targetField ] === 'string'
22
+ ? ( params [ config . targetField ] as string )
23
+ : undefined ;
24
+
25
+ return {
26
+ target,
27
+ attributes : target && config . targetAttribute ? { [ config . targetAttribute ] : target } : { } ,
28
+ } ;
29
+ }
30
+
31
+ /**
32
+ * Extracts request arguments based on method type
33
+ */
34
+ export function getRequestArguments ( method : string , params : Record < string , unknown > ) : Record < string , string > {
35
+ const args : Record < string , string > = { } ;
36
+ const config = METHOD_CONFIGS [ method as keyof typeof METHOD_CONFIGS ] ;
37
+
38
+ if ( ! config ) {
39
+ return args ;
40
+ }
41
+
42
+ // Capture arguments from the configured field
43
+ if ( config . captureArguments && config . argumentsField && params ?. [ config . argumentsField ] ) {
44
+ const argumentsObj = params [ config . argumentsField ] ;
45
+ if ( typeof argumentsObj === 'object' && argumentsObj !== null ) {
46
+ for ( const [ key , value ] of Object . entries ( argumentsObj as Record < string , unknown > ) ) {
47
+ args [ `mcp.request.argument.${ key . toLowerCase ( ) } ` ] = JSON . stringify ( value ) ;
48
+ }
49
+ }
50
+ }
51
+
52
+ // Capture specific fields as arguments
53
+ if ( config . captureUri && params ?. uri ) {
54
+ args [ 'mcp.request.argument.uri' ] = JSON . stringify ( params . uri ) ;
55
+ }
56
+
57
+ if ( config . captureName && params ?. name ) {
58
+ args [ 'mcp.request.argument.name' ] = JSON . stringify ( params . name ) ;
59
+ }
60
+
61
+ return args ;
62
+ }
63
+
64
+ /**
65
+ * Extracts additional attributes for specific notification types
66
+ */
67
+ export function getNotificationAttributes ( method : string , params : Record < string , unknown > ) : Record < string , string | number > {
68
+ const attributes : Record < string , string | number > = { } ;
69
+
70
+ switch ( method ) {
71
+ case 'notifications/cancelled' :
72
+ if ( params ?. requestId ) {
73
+ attributes [ 'mcp.cancelled.request_id' ] = String ( params . requestId ) ;
74
+ }
75
+ if ( params ?. reason ) {
76
+ attributes [ 'mcp.cancelled.reason' ] = String ( params . reason ) ;
77
+ }
78
+ break ;
79
+
80
+ case 'notifications/message' :
81
+ if ( params ?. level ) {
82
+ attributes [ 'mcp.logging.level' ] = String ( params . level ) ;
83
+ }
84
+ if ( params ?. logger ) {
85
+ attributes [ 'mcp.logging.logger' ] = String ( params . logger ) ;
86
+ }
87
+ if ( params ?. data !== undefined ) {
88
+ attributes [ 'mcp.logging.data_type' ] = typeof params . data ;
89
+ // Store the actual message content
90
+ if ( typeof params . data === 'string' ) {
91
+ attributes [ 'mcp.logging.message' ] = params . data ;
92
+ } else {
93
+ attributes [ 'mcp.logging.message' ] = JSON . stringify ( params . data ) ;
94
+ }
95
+ }
96
+ break ;
97
+
98
+ case 'notifications/progress' :
99
+ if ( params ?. progressToken ) {
100
+ attributes [ 'mcp.progress.token' ] = String ( params . progressToken ) ;
101
+ }
102
+ if ( typeof params ?. progress === 'number' ) {
103
+ attributes [ 'mcp.progress.current' ] = params . progress ;
104
+ }
105
+ if ( typeof params ?. total === 'number' ) {
106
+ attributes [ 'mcp.progress.total' ] = params . total ;
107
+ if ( typeof params ?. progress === 'number' ) {
108
+ attributes [ 'mcp.progress.percentage' ] = ( params . progress / params . total ) * 100 ;
109
+ }
110
+ }
111
+ if ( params ?. message ) {
112
+ attributes [ 'mcp.progress.message' ] = String ( params . message ) ;
113
+ }
114
+ break ;
115
+
116
+ case 'notifications/resources/updated' :
117
+ if ( params ?. uri ) {
118
+ attributes [ 'mcp.resource.uri' ] = String ( params . uri ) ;
119
+ // Extract protocol from URI
120
+ const urlObject = parseStringToURLObject ( String ( params . uri ) ) ;
121
+ if ( urlObject && ! isURLObjectRelative ( urlObject ) ) {
122
+ attributes [ 'mcp.resource.protocol' ] = urlObject . protocol . replace ( ':' , '' ) ;
123
+ }
124
+ }
125
+ break ;
126
+
127
+ case 'notifications/initialized' :
128
+ attributes [ 'mcp.lifecycle.phase' ] = 'initialization_complete' ;
129
+ attributes [ 'mcp.protocol.ready' ] = 1 ;
130
+ break ;
131
+ }
132
+
133
+ return attributes ;
134
+ }
135
+
136
+ /**
137
+ * Extracts attributes from tool call results for tracking
138
+ * Captures actual content for debugging and monitoring
139
+ *
140
+ * @param method The MCP method name (should be 'tools/call')
141
+ * @param result The raw CallToolResult object returned by the tool handler
142
+ */
143
+ export function extractToolResultAttributes (
144
+ method : string ,
145
+ result : unknown ,
146
+ ) : Record < string , string | number | boolean > {
147
+ const attributes : Record < string , string | number | boolean > = { } ;
148
+
149
+ // Only process tool call results
150
+ if ( method !== 'tools/call' || ! result || typeof result !== 'object' ) {
151
+ return attributes ;
152
+ }
153
+
154
+ // The result is the raw CallToolResult object from the tool handler
155
+ const toolResult = result as {
156
+ content ?: Array < { type ?: string ; text ?: string ; [ key : string ] : unknown } > ;
157
+ structuredContent ?: Record < string , unknown > ;
158
+ isError ?: boolean ;
159
+ } ;
160
+
161
+ // Track if result is an error
162
+ if ( toolResult . isError !== undefined ) {
163
+ attributes [ 'mcp.tool.result.is_error' ] = toolResult . isError ;
164
+ }
165
+
166
+ // Track content metadata and actual content
167
+ if ( toolResult . content && Array . isArray ( toolResult . content ) ) {
168
+ attributes [ 'mcp.tool.result.content_count' ] = toolResult . content . length ;
169
+
170
+ // Track content types
171
+ const types = toolResult . content . map ( c => c . type ) . filter ( ( type ) : type is string => typeof type === 'string' ) ;
172
+
173
+ if ( types . length > 0 ) {
174
+ attributes [ 'mcp.tool.result.content_types' ] = types . join ( ',' ) ;
175
+ }
176
+
177
+ // Track actual content - serialize the full content array
178
+ try {
179
+ attributes [ 'mcp.tool.result.content' ] = JSON . stringify ( toolResult . content ) ;
180
+ } catch ( error ) {
181
+ // If serialization fails, store a fallback message
182
+ attributes [ 'mcp.tool.result.content' ] = '[Content serialization failed]' ;
183
+ }
184
+ }
185
+
186
+ // Track structured content if exists
187
+ if ( toolResult . structuredContent !== undefined ) {
188
+ attributes [ 'mcp.tool.result.has_structured_content' ] = true ;
189
+
190
+ // Track actual structured content
191
+ try {
192
+ attributes [ 'mcp.tool.result.structured_content' ] = JSON . stringify ( toolResult . structuredContent ) ;
193
+ } catch ( error ) {
194
+ // If serialization fails, store a fallback message
195
+ attributes [ 'mcp.tool.result.structured_content' ] = '[Structured content serialization failed]' ;
196
+ }
197
+ }
198
+
199
+ return attributes ;
200
+ }
201
+
202
+ /**
203
+ * Extracts arguments from handler parameters for handler-level instrumentation
204
+ */
205
+ export function extractHandlerArguments ( handlerType : string , args : unknown [ ] ) : Record < string , string > {
206
+ const arguments_ : Record < string , string > = { } ;
207
+
208
+ // Find the first argument that is not the extra object
209
+ const firstArg = args . find ( arg =>
210
+ arg &&
211
+ typeof arg === 'object' &&
212
+ ! ( 'requestId' in arg )
213
+ ) ;
214
+
215
+ if ( ! firstArg ) {
216
+ return arguments_ ;
217
+ }
218
+
219
+ if ( handlerType === 'tool' || handlerType === 'prompt' ) {
220
+ // For tools and prompts, first arg contains the arguments
221
+ if ( typeof firstArg === 'object' && firstArg !== null ) {
222
+ for ( const [ key , value ] of Object . entries ( firstArg as Record < string , unknown > ) ) {
223
+ arguments_ [ `mcp.request.argument.${ key . toLowerCase ( ) } ` ] = typeof value === 'string' ? value : JSON . stringify ( value ) ;
224
+ }
225
+ }
226
+ } else if ( handlerType === 'resource' ) {
227
+ // For resources, we might have URI and variables
228
+ // First argument is usually the URI (resource name)
229
+ // Second argument might be variables for template expansion
230
+ const uriArg = args [ 0 ] ;
231
+ if ( typeof uriArg === 'string' || uriArg instanceof URL ) {
232
+ arguments_ [ 'mcp.request.argument.uri' ] = JSON . stringify ( uriArg . toString ( ) ) ;
233
+ }
234
+
235
+ // Check if second argument is variables (not the extra object)
236
+ const secondArg = args [ 1 ] ;
237
+ if ( secondArg && typeof secondArg === 'object' && ! ( 'requestId' in secondArg ) ) {
238
+ for ( const [ key , value ] of Object . entries ( secondArg as Record < string , unknown > ) ) {
239
+ arguments_ [ `mcp.request.argument.${ key . toLowerCase ( ) } ` ] = typeof value === 'string' ? value : JSON . stringify ( value ) ;
240
+ }
241
+ }
242
+ }
243
+
244
+ return arguments_ ;
245
+ }
246
+
247
+ /**
248
+ * Extracts client connection information
249
+ */
250
+ export function extractClientInfo ( extra : ExtraHandlerData ) : {
251
+ address ?: string ;
252
+ port ?: number ;
253
+ } {
254
+ return {
255
+ address :
256
+ extra ?. requestInfo ?. remoteAddress ||
257
+ extra ?. clientAddress ||
258
+ extra ?. request ?. ip ||
259
+ extra ?. request ?. connection ?. remoteAddress ,
260
+ port : extra ?. requestInfo ?. remotePort || extra ?. clientPort || extra ?. request ?. connection ?. remotePort ,
261
+ } ;
262
+ }
0 commit comments