1
1
import logging
2
2
from typing import Any , Collection
3
3
4
- from openinference .instrumentation .mcp .package import _instruments
5
4
from wrapt import register_post_import_hook , wrap_function_wrapper
6
5
7
6
from opentelemetry import trace
8
7
from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
9
8
from opentelemetry .instrumentation .utils import unwrap
10
9
10
+ from .package import _instruments
11
+
11
12
12
13
def setup_logger_two ():
13
14
logger = logging .getLogger ("loggertwo" )
@@ -21,80 +22,11 @@ def setup_logger_two():
21
22
return logger
22
23
23
24
24
- logger_two = setup_logger_two ()
25
-
26
-
27
25
class MCPInstrumentor (BaseInstrumentor ):
28
26
"""
29
27
An instrumenter for MCP.
30
28
"""
31
29
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
-
98
30
# Send Request Wrapper
99
31
def _wrap_send_request (self , wrapped , instance , args , kwargs ):
100
32
"""
@@ -106,11 +38,7 @@ def _wrap_send_request(self, wrapped, instance, args, kwargs):
106
38
"""
107
39
108
40
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 :
114
42
span_ctx = span .get_span_context ()
115
43
request = args [0 ] if len (args ) > 0 else kwargs .get ("request" )
116
44
if request :
@@ -133,19 +61,6 @@ async def async_wrapper():
133
61
134
62
return async_wrapper ()
135
63
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
-
149
64
# Handle Request Wrapper
150
65
async def _wrap_handle_request (self , wrapped , instance , args , kwargs ):
151
66
"""
@@ -154,29 +69,20 @@ async def _wrap_handle_request(self, wrapped, instance, args, kwargs):
154
69
the request's params._meta field, and creates server-side OpenTelemetry spans linked to the client spans.
155
70
The wrapper also does not change the original function's behavior by calling it with identical parameters
156
71
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"
157
76
"""
158
77
req = args [1 ] if len (args ) > 1 else None
159
- trace_context = None
78
+ traceparent = None
160
79
161
80
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 :
178
84
span_name = self ._get_span_name (req )
179
- with tracer .start_as_current_span (
85
+ with self . tracer .start_as_current_span (
180
86
span_name ,
181
87
kind = trace .SpanKind .SERVER ,
182
88
context = trace .set_span_in_context (trace .NonRecordingSpan (span_context )),
@@ -186,3 +92,101 @@ async def _wrap_handle_request(self, wrapped, instance, args, kwargs):
186
92
return result
187
93
else :
188
94
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