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