Skip to content

Commit 5f70570

Browse files
committed
fixes
1 parent c78df18 commit 5f70570

File tree

7 files changed

+81
-134
lines changed

7 files changed

+81
-134
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/mcp/__init__.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/mcp/constants.py

Lines changed: 0 additions & 38 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from opentelemetry.instrumentation.mcp.version import __version__
5+
from opentelemetry.instrumentation.mcp.instrumentation import McpInstrumentor
6+
7+
__all__ = ["McpInstrumentor", "__version__"]
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,33 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
from typing import Any, Callable, Collection, Dict, Tuple
3+
from typing import Any, Callable, Collection, Dict, Optional, Tuple
44

55
from wrapt import register_post_import_hook, wrap_function_wrapper
66

77
from opentelemetry import trace
88
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
99
from opentelemetry.instrumentation.utils import unwrap
1010
from opentelemetry.semconv.trace import SpanAttributes
11+
from opentelemetry.instrumentation.mcp.version import __version__
12+
from opentelemetry.propagate import get_global_textmap
1113

12-
from .constants import MCPEnvironmentVariables, MCPTraceContext
13-
from .semconv import MCPAttributes, MCPOperations, MCPSpanNames
14+
from .semconv import CLIENT_INITIALIZED, MCP_METHOD_NAME, TOOLS_CALL, TOOLS_LIST, MCPAttributes, MCPOperations, MCPSpanNames
1415

1516

16-
class MCPInstrumentor(BaseInstrumentor):
17+
class McpInstrumentor(BaseInstrumentor):
1718
"""
1819
An instrumenter for MCP.
1920
"""
2021

21-
def __init__(self):
22+
def __init__(self, **kwargs):
2223
super().__init__()
23-
self.tracer = None
24+
self.propagators = kwargs.get("propagators") or get_global_textmap()
25+
self.tracer = trace.get_tracer(__name__, __version__, tracer_provider=kwargs.get("tracer_provider", None))
2426

25-
@staticmethod
26-
def instrumentation_dependencies() -> Collection[str]:
27-
return ("mcp >= 1.6.0",)
27+
def instrumentation_dependencies(self) -> Collection[str]:
28+
return "mcp >= 1.6.0"
2829

2930
def _instrument(self, **kwargs: Any) -> None:
30-
tracer_provider = kwargs.get("tracer_provider")
31-
if tracer_provider:
32-
self.tracer = tracer_provider.get_tracer("instrumentation.mcp")
33-
else:
34-
self.tracer = trace.get_tracer("instrumentation.mcp")
3531
register_post_import_hook(
3632
lambda _: wrap_function_wrapper(
3733
"mcp.shared.session",
@@ -49,48 +45,61 @@ def _instrument(self, **kwargs: Any) -> None:
4945
"mcp.server.lowlevel.server",
5046
)
5147

52-
@staticmethod
53-
def _uninstrument(**kwargs: Any) -> None:
48+
def _uninstrument(self, **kwargs: Any) -> None:
5449
unwrap("mcp.shared.session", "BaseSession.send_request")
5550
unwrap("mcp.server.lowlevel.server", "Server._handle_request")
56-
57-
# Send Request Wrapper
51+
52+
5853
def _wrap_send_request(
5954
self, wrapped: Callable, instance: Any, args: Tuple[Any, ...], kwargs: Dict[str, Any]
6055
) -> Callable:
61-
"""
62-
Changes made:
63-
The wrapper intercepts the request before sending, injects distributed tracing context into the
64-
request's params._meta field and creates OpenTelemetry spans. The wrapper does not change anything
65-
else from the original function's behavior because it reconstructs the request object with the same
66-
type and calling the original function with identical parameters.
56+
import mcp.types as types
57+
"""
58+
Patches BaseSession.send_request which is responsible for sending requests from the client to the MCP server.
59+
This patched MCP client intercepts the request to obtain attributes for creating client-side span, extracts
60+
the current trace context, and embeds it into the request's params._meta.traceparent field
61+
before forwarding the request to the MCP server.
62+
63+
Args:
64+
wrapped: The original BaseSession.send_request function
65+
instance: The BaseSession instance
66+
args: Positional arguments, where args[0] is typically the request object
67+
kwargs: Keyword arguments, may contain 'request' parameter
68+
69+
Returns:
70+
Callable: Async wrapper function that handles trace context injection
6771
"""
6872

6973
async def async_wrapper():
74+
request: Optional[types.ClientRequest] = args[0] if len(args) > 0 else None
75+
76+
if not request:
77+
return await wrapped(*args, **kwargs)
78+
7079
with self.tracer.start_as_current_span(
7180
MCPSpanNames.CLIENT_SEND_REQUEST, kind=trace.SpanKind.CLIENT
7281
) as span:
73-
span_ctx = span.get_span_context()
74-
request = args[0] if len(args) > 0 else kwargs.get("request")
82+
7583
if request:
76-
req_root = request.root if hasattr(request, "root") else request
77-
78-
self._generate_mcp_attributes(span, req_root, is_client=True)
84+
span_ctx = trace.set_span_in_context(span)
85+
parent_span = {}
86+
self.propagators.inject(carrier=parent_span, context=span_ctx)
87+
7988
request_data = request.model_dump(by_alias=True, mode="json", exclude_none=True)
80-
self._inject_trace_context(request_data, span_ctx)
89+
90+
if "params" not in request_data:
91+
request_data["params"] = {}
92+
if "_meta" not in request_data["params"]:
93+
request_data["params"]["_meta"] = {}
94+
request_data["params"]["_meta"].update(parent_span)
95+
8196
# Reconstruct request object with injected trace context
82-
modified_request = type(request).model_validate(request_data)
83-
if len(args) > 0:
84-
new_args = (modified_request,) + args[1:]
85-
result = await wrapped(*new_args, **kwargs)
86-
else:
87-
kwargs["request"] = modified_request
88-
result = await wrapped(*args, **kwargs)
89-
else:
90-
result = await wrapped(*args, **kwargs)
91-
return result
97+
modified_request = request.model_validate(request_data)
98+
new_args = (modified_request,) + args[1:]
99+
100+
return await wrapped(*new_args, **kwargs)
92101

93-
return async_wrapper()
102+
return async_wrapper
94103

95104
# Handle Request Wrapper
96105
async def _wrap_handle_request(
@@ -111,7 +120,7 @@ async def _wrap_handle_request(
111120
traceparent = None
112121

113122
if req and hasattr(req, "params") and req.params and hasattr(req.params, "meta") and req.params.meta:
114-
traceparent = getattr(req.params.meta, MCPTraceContext.TRACEPARENT_HEADER, None)
123+
traceparent = None
115124
span_context = self._extract_span_context_from_traceparent(traceparent) if traceparent else None
116125
if span_context:
117126
span_name = self._get_mcp_operation(req)
@@ -130,40 +139,18 @@ async def _wrap_handle_request(
130139
def _generate_mcp_attributes(span: trace.Span, request: Any, is_client: bool) -> None:
131140
import mcp.types as types # pylint: disable=import-outside-toplevel,consider-using-from-import
132141

133-
operation = MCPOperations.UNKNOWN_OPERATION
134-
135142
if isinstance(request, types.ListToolsRequest):
136-
operation = MCPOperations.LIST_TOOL
137-
span.set_attribute(MCPAttributes.MCP_LIST_TOOLS, True)
143+
span.set_attribute(MCP_METHOD_NAME, TOOLS_LIST)
138144
if is_client:
139145
span.update_name(MCPSpanNames.CLIENT_LIST_TOOLS)
140146
elif isinstance(request, types.CallToolRequest):
141-
operation = request.params.name
142-
span.set_attribute(MCPAttributes.MCP_CALL_TOOL, True)
147+
span.set_attribute(MCP_METHOD_NAME, TOOLS_CALL)
143148
if is_client:
144149
span.update_name(MCPSpanNames.client_call_tool(request.params.name))
145150
elif isinstance(request, types.InitializeRequest):
146-
operation = MCPOperations.INITIALIZE
147-
span.set_attribute(MCPAttributes.MCP_INITIALIZE, True)
148-
if is_client:
149-
span.update_name(MCPSpanNames.CLIENT_INITIALIZE)
151+
span.set_attribute(MCP_METHOD_NAME, CLIENT_INITIALIZED)
150152

151-
if is_client:
152-
MCPInstrumentor._add_client_attributes(span, operation, request)
153-
else:
154-
MCPInstrumentor._add_server_attributes(span, operation, request)
155-
156-
@staticmethod
157-
def _inject_trace_context(request_data: Dict[str, Any], span_ctx) -> None:
158-
if "params" not in request_data:
159-
request_data["params"] = {}
160-
if "_meta" not in request_data["params"]:
161-
request_data["params"]["_meta"] = {}
162-
trace_id_hex = f"{span_ctx.trace_id:032x}"
163-
span_id_hex = f"{span_ctx.span_id:016x}"
164-
trace_flags = MCPTraceContext.TRACE_FLAGS_SAMPLED
165-
traceparent = f"{MCPTraceContext.TRACEPARENT_VERSION}-{trace_id_hex}-{span_id_hex}-{trace_flags}"
166-
request_data["params"]["_meta"][MCPTraceContext.TRACEPARENT_HEADER] = traceparent
153+
# Additional attributes can be added here if needed
167154

168155
@staticmethod
169156
def _extract_span_context_from_traceparent(traceparent: str):
@@ -195,18 +182,3 @@ def _get_mcp_operation(req: Any) -> str:
195182
elif isinstance(req, types.CallToolRequest):
196183
span_name = MCPSpanNames.tools_call(req.params.name)
197184
return span_name
198-
199-
@staticmethod
200-
def _add_client_attributes(span: trace.Span, operation: str, request: Any) -> None:
201-
import os # pylint: disable=import-outside-toplevel
202-
203-
service_name = os.environ.get(MCPEnvironmentVariables.SERVER_NAME, "mcp server")
204-
span.set_attribute(SpanAttributes.RPC_SERVICE, service_name)
205-
span.set_attribute(SpanAttributes.RPC_METHOD, operation)
206-
if hasattr(request, "params") and request.params and hasattr(request.params, "name"):
207-
span.set_attribute(MCPAttributes.MCP_TOOL_NAME, request.params.name)
208-
209-
@staticmethod
210-
def _add_server_attributes(span: trace.Span, operation: str, request: Any) -> None:
211-
if hasattr(request, "params") and request.params and hasattr(request.params, "name"):
212-
span.set_attribute(MCPAttributes.MCP_TOOL_NAME, request.params.name)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/mcp/semconv.py renamed to aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/semconv.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@
33

44
"""
55
MCP (Model Context Protocol) Semantic Conventions for OpenTelemetry.
6-
7-
This module defines semantic conventions for MCP instrumentation following
8-
OpenTelemetry standards for consistent telemetry data.
96
"""
107

118

9+
MCP_METHOD_NAME = "mcp.method.name"
10+
MCP_REQUEST_ID = "mcp.request.id"
11+
MCP_SESSION_ID = "mcp.session.id"
12+
MCP_TOOL_NAME = "mcp.tool.name"
13+
MCP_PROMPT_NAME = "mcp.prompt.name"
14+
MCP_REQUEST_ARGUMENT = "mcp.request.argument"
15+
16+
17+
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
18+
NOTIFICATIONS_INITIALIZED = "notifications/initialized"
19+
NOTIFICATIONS_PROGRESS = "notifications/progress"
20+
RESOURCES_LIST = "resources/list"
21+
TOOLS_LIST = "tools/list"
22+
TOOLS_CALL = "tools/call"
23+
CLIENT_INITIALIZED = "initialize"
24+
25+
1226
class MCPAttributes:
13-
"""MCP-specific span attributes for OpenTelemetry instrumentation."""
1427

1528
# MCP Operation Type Attributes
1629
MCP_INITIALIZE = "notifications/initialize"
@@ -43,18 +56,13 @@ class MCPSpanNames:
4356
"""Standard span names for MCP operations."""
4457

4558
# Client-side span names
46-
CLIENT_SEND_REQUEST = "client.send_request"
59+
CLIENT_SEND_REQUEST = "span.mcp.client"
4760
"""
4861
Span name for client-side MCP request operations.
4962
Used for all outgoing MCP requests (initialize, list tools, call tool).
5063
"""
5164

52-
CLIENT_INITIALIZE = "notifications/initialize"
53-
"""
54-
Span name for client-side MCP initialization requests.
55-
"""
56-
57-
CLIENT_LIST_TOOLS = "mcp.list_tools"
65+
CLIENT_LIST_TOOLS = "span.mcp.server"
5866
"""
5967
Span name for client-side MCP list tools requests.
6068
"""

0 commit comments

Comments
 (0)