Skip to content

Commit 10d91ac

Browse files
committed
Fixed mcp supported python
1 parent 442a45a commit 10d91ac

File tree

5 files changed

+115
-109
lines changed

5 files changed

+115
-109
lines changed

aws-opentelemetry-distro/loggertwo.log

Whitespace-only changes.
Lines changed: 110 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import logging
22
from typing import Any, Collection
33

4-
from openinference.instrumentation.mcp.package import _instruments
54
from wrapt import register_post_import_hook, wrap_function_wrapper
65

76
from opentelemetry import trace
87
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
98
from opentelemetry.instrumentation.utils import unwrap
109

10+
from .package import _instruments
11+
1112

1213
def 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-
2725
class 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")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_instruments = ("mcp >= 1.6.0",)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/mcpinstrumentor/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ version = "0.1.0"
88
description = "OpenTelemetry MCP instrumentation for AWS Distro"
99
readme = "README.md"
1010
license = "Apache-2.0"
11-
requires-python = ">=3.9"
11+
requires-python = ">=3.10"
1212
authors = [
1313
{ name = "Johnny Lin", email = "[email protected]" },
1414
]
@@ -18,17 +18,17 @@ classifiers = [
1818
"License :: OSI Approved :: Apache Software License",
1919
"Programming Language :: Python",
2020
"Programming Language :: Python :: 3",
21-
"Programming Language :: Python :: 3.9",
2221
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
2423
"Programming Language :: Python :: 3.12",
2524
"Programming Language :: Python :: 3.13",
2625
]
2726
dependencies = [
28-
"openinference-instrumentation-mcp",
27+
"mcp",
2928
"opentelemetry-api",
3029
"opentelemetry-instrumentation",
3130
"opentelemetry-sdk",
31+
"opentelemetry-semantic-conventions",
3232
"wrapt"
3333
]
3434

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

0 commit comments

Comments
 (0)