11import logging
22from typing import Any , Collection
33
4- from openinference .instrumentation .mcp .package import _instruments
54from wrapt import register_post_import_hook , wrap_function_wrapper
65
76from opentelemetry import trace
87from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
98from opentelemetry .instrumentation .utils import unwrap
109
10+ from .package import _instruments
11+
1112
1213def setup_logger_two ():
1314 logger = logging .getLogger ("loggertwo" )
@@ -21,80 +22,11 @@ def setup_logger_two():
2122 return logger
2223
2324
24- logger_two = setup_logger_two ()
25-
26-
2725class MCPInstrumentor (BaseInstrumentor ):
2826 """
2927 An instrumenter for MCP.
3028 """
3129
32- def instrumentation_dependencies (self ) -> Collection [str ]:
33- return _instruments
34-
35- def _instrument (self , ** kwargs : Any ) -> None :
36- tracer_provider = kwargs .get ("tracer_provider" )
37- if tracer_provider :
38- self .tracer_provider = tracer_provider
39- else :
40- self .tracer_provider = None
41- register_post_import_hook (
42- lambda _ : wrap_function_wrapper (
43- "mcp.shared.session" ,
44- "BaseSession.send_request" ,
45- self ._wrap_send_request ,
46- ),
47- "mcp.shared.session" ,
48- )
49- register_post_import_hook (
50- lambda _ : wrap_function_wrapper (
51- "mcp.server.lowlevel.server" ,
52- "Server._handle_request" ,
53- self ._wrap_handle_request ,
54- ),
55- "mcp.server.lowlevel.server" ,
56- )
57-
58- def _uninstrument (self , ** kwargs : Any ) -> None :
59- unwrap ("mcp.shared.session" , "BaseSession.send_request" )
60- unwrap ("mcp.server.lowlevel.server" , "Server._handle_request" )
61-
62- def handle_attributes (self , span , request , is_client = True ):
63- import mcp .types as types
64-
65- operation = "Server Handle Request"
66- if isinstance (request , types .ListToolsRequest ):
67- operation = "ListTool"
68- span .set_attribute ("mcp.list_tools" , True )
69- elif isinstance (request , types .CallToolRequest ):
70- if hasattr (request , "params" ) and hasattr (request .params , "name" ):
71- operation = request .params .name
72- span .set_attribute ("mcp.call_tool" , True )
73- if is_client :
74- self ._add_client_attributes (span , operation , request )
75- else :
76- self ._add_server_attributes (span , operation , request )
77-
78- def _add_client_attributes (self , span , operation , request ):
79- span .set_attribute ("span.kind" , "CLIENT" )
80- span .set_attribute ("aws.remote.service" , "Appsignals MCP Server" )
81- span .set_attribute ("aws.remote.operation" , operation )
82- if hasattr (request , "params" ) and hasattr (request .params , "name" ):
83- span .set_attribute ("tool.name" , request .params .name )
84-
85- def _add_server_attributes (self , span , operation , request ):
86- span .set_attribute ("server_side" , True )
87- span .set_attribute ("aws.span.kind" , "SERVER" )
88- if hasattr (request , "params" ) and hasattr (request .params , "name" ):
89- span .set_attribute ("tool.name" , request .params .name )
90-
91- def _inject_trace_context (self , request_data , span_ctx ):
92- if "params" not in request_data :
93- request_data ["params" ] = {}
94- if "_meta" not in request_data ["params" ]:
95- request_data ["params" ]["_meta" ] = {}
96- request_data ["params" ]["_meta" ]["trace_context" ] = {"trace_id" : span_ctx .trace_id , "span_id" : span_ctx .span_id }
97-
9830 # Send Request Wrapper
9931 def _wrap_send_request (self , wrapped , instance , args , kwargs ):
10032 """
@@ -106,11 +38,7 @@ def _wrap_send_request(self, wrapped, instance, args, kwargs):
10638 """
10739
10840 async def async_wrapper ():
109- if self .tracer_provider is None :
110- tracer = trace .get_tracer ("mcp.client" )
111- else :
112- tracer = self .tracer_provider .get_tracer ("mcp.client" )
113- with tracer .start_as_current_span ("client.send_request" , kind = trace .SpanKind .CLIENT ) as span :
41+ with self .tracer .start_as_current_span ("client.send_request" , kind = trace .SpanKind .CLIENT ) as span :
11442 span_ctx = span .get_span_context ()
11543 request = args [0 ] if len (args ) > 0 else kwargs .get ("request" )
11644 if request :
@@ -133,19 +61,6 @@ async def async_wrapper():
13361
13462 return async_wrapper ()
13563
136- def _get_span_name (self , req ):
137- span_name = "unknown"
138- import mcp .types as types
139-
140- if isinstance (req , types .ListToolsRequest ):
141- span_name = "tools/list"
142- elif isinstance (req , types .CallToolRequest ):
143- if hasattr (req , "params" ) and hasattr (req .params , "name" ):
144- span_name = f"tools/{ req .params .name } "
145- else :
146- span_name = "unknown"
147- return span_name
148-
14964 # Handle Request Wrapper
15065 async def _wrap_handle_request (self , wrapped , instance , args , kwargs ):
15166 """
@@ -154,29 +69,20 @@ async def _wrap_handle_request(self, wrapped, instance, args, kwargs):
15469 the request's params._meta field, and creates server-side OpenTelemetry spans linked to the client spans.
15570 The wrapper also does not change the original function's behavior by calling it with identical parameters
15671 ensuring no breaking changes to the MCP server functionality.
72+
73+ request (args[1]) is typically an instance of CallToolRequest or ListToolsRequest
74+ and should have the structure:
75+ request.params.meta.traceparent -> "00-<trace_id>-<span_id>-01"
15776 """
15877 req = args [1 ] if len (args ) > 1 else None
159- trace_context = None
78+ traceparent = None
16079
16180 if req and hasattr (req , "params" ) and req .params and hasattr (req .params , "meta" ) and req .params .meta :
162- trace_context = req .params .meta .trace_context
163- if trace_context :
164-
165- if self .tracer_provider is None :
166- tracer = trace .get_tracer ("mcp.server" )
167- else :
168- tracer = self .tracer_provider .get_tracer ("mcp.server" )
169- trace_id = trace_context .get ("trace_id" )
170- span_id = trace_context .get ("span_id" )
171- span_context = trace .SpanContext (
172- trace_id = trace_id ,
173- span_id = span_id ,
174- is_remote = True ,
175- trace_flags = trace .TraceFlags (trace .TraceFlags .SAMPLED ),
176- trace_state = trace .TraceState (),
177- )
81+ traceparent = getattr (req .params .meta , "traceparent" , None )
82+ span_context = self ._extract_span_context_from_traceparent (traceparent ) if traceparent else None
83+ if span_context :
17884 span_name = self ._get_span_name (req )
179- with tracer .start_as_current_span (
85+ with self . tracer .start_as_current_span (
18086 span_name ,
18187 kind = trace .SpanKind .SERVER ,
18288 context = trace .set_span_in_context (trace .NonRecordingSpan (span_context )),
@@ -186,3 +92,101 @@ async def _wrap_handle_request(self, wrapped, instance, args, kwargs):
18692 return result
18793 else :
18894 return await wrapped (* args , ** kwargs )
95+
96+ def _inject_trace_context (self , request_data , span_ctx ):
97+ if "params" not in request_data :
98+ request_data ["params" ] = {}
99+ if "_meta" not in request_data ["params" ]:
100+ request_data ["params" ]["_meta" ] = {}
101+ trace_id_hex = f"{ span_ctx .trace_id :032x} "
102+ span_id_hex = f"{ span_ctx .span_id :016x} "
103+ trace_flags = "01"
104+ traceparent = f"00-{ trace_id_hex } -{ span_id_hex } -{ trace_flags } "
105+ request_data ["params" ]["_meta" ]["traceparent" ] = traceparent
106+
107+ def _extract_span_context_from_traceparent (self , traceparent ):
108+ parts = traceparent .split ("-" )
109+ if len (parts ) == 4 :
110+ try :
111+ trace_id = int (parts [1 ], 16 )
112+ span_id = int (parts [2 ], 16 )
113+ return trace .SpanContext (
114+ trace_id = trace_id ,
115+ span_id = span_id ,
116+ is_remote = True ,
117+ trace_flags = trace .TraceFlags (trace .TraceFlags .SAMPLED ),
118+ trace_state = trace .TraceState (),
119+ )
120+ except ValueError :
121+ return None
122+ return None
123+
124+ def _get_span_name (self , req ):
125+ span_name = "unknown"
126+ import mcp .types as types
127+
128+ if isinstance (req , types .ListToolsRequest ):
129+ span_name = "tools/list"
130+ elif isinstance (req , types .CallToolRequest ):
131+ if hasattr (req , "params" ) and hasattr (req .params , "name" ):
132+ span_name = f"tools/{ req .params .name } "
133+ else :
134+ span_name = "unknown"
135+ return span_name
136+
137+ def handle_attributes (self , span , request , is_client = True ):
138+ import mcp .types as types
139+
140+ operation = self ._get_span_name (request )
141+ if isinstance (request , types .ListToolsRequest ):
142+ operation = "ListTool"
143+ span .set_attribute ("mcp.list_tools" , True )
144+ elif isinstance (request , types .CallToolRequest ):
145+ if hasattr (request , "params" ) and hasattr (request .params , "name" ):
146+ operation = request .params .name
147+ span .set_attribute ("mcp.call_tool" , True )
148+ if is_client :
149+ self ._add_client_attributes (span , operation , request )
150+ else :
151+ self ._add_server_attributes (span , operation , request )
152+
153+ def _add_client_attributes (self , span , operation , request ):
154+ span .set_attribute ("aws.remote.service" , "Appsignals MCP Server" )
155+ span .set_attribute ("aws.remote.operation" , operation )
156+ if hasattr (request , "params" ) and hasattr (request .params , "name" ):
157+ span .set_attribute ("tool.name" , request .params .name )
158+
159+ def _add_server_attributes (self , span , operation , request ):
160+ span .set_attribute ("server_side" , True )
161+ if hasattr (request , "params" ) and hasattr (request .params , "name" ):
162+ span .set_attribute ("tool.name" , request .params .name )
163+
164+ def instrumentation_dependencies (self ) -> Collection [str ]:
165+ return _instruments
166+
167+ def _instrument (self , ** kwargs : Any ) -> None :
168+ tracer_provider = kwargs .get ("tracer_provider" )
169+ if tracer_provider :
170+ self .tracer = tracer_provider .get_tracer ("mcp" )
171+ else :
172+ self .tracer = trace .get_tracer ("mcp" )
173+ register_post_import_hook (
174+ lambda _ : wrap_function_wrapper (
175+ "mcp.shared.session" ,
176+ "BaseSession.send_request" ,
177+ self ._wrap_send_request ,
178+ ),
179+ "mcp.shared.session" ,
180+ )
181+ register_post_import_hook (
182+ lambda _ : wrap_function_wrapper (
183+ "mcp.server.lowlevel.server" ,
184+ "Server._handle_request" ,
185+ self ._wrap_handle_request ,
186+ ),
187+ "mcp.server.lowlevel.server" ,
188+ )
189+
190+ def _uninstrument (self , ** kwargs : Any ) -> None :
191+ unwrap ("mcp.shared.session" , "BaseSession.send_request" )
192+ unwrap ("mcp.server.lowlevel.server" , "Server._handle_request" )
0 commit comments