Skip to content

Commit 0af52d9

Browse files
committed
Fixed span attributes, updated mcp model, lint, semconv changes
1 parent 70b0da3 commit 0af52d9

File tree

5 files changed

+69
-81
lines changed

5 files changed

+69
-81
lines changed

aws-opentelemetry-distro/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ dependencies = [
8989
# If a new patch is added into the list, it must also be added into tox.ini, dev-requirements.txt and _instrumentation_patch
9090
patch = [
9191
"botocore ~= 1.0",
92-
"mcp >= 1.1.0"
92+
"mcp >= 1.6.0"
9393
]
9494
test = []
9595

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
MCP (Model Context Protocol) Constants for OpenTelemetry instrumentation.
6+
7+
This module defines constants and configuration variables used by the MCP instrumentor.
8+
"""
9+
10+
11+
class MCPTraceContext:
12+
"""Constants for MCP distributed tracing context propagation."""
13+
14+
TRACEPARENT_HEADER = "traceparent"
15+
"""
16+
W3C Trace Context traceparent header name.
17+
Used for propagating trace context in MCP request metadata.
18+
"""
19+
20+
TRACE_FLAGS_SAMPLED = "01"
21+
"""
22+
W3C Trace Context flags indicating the trace is sampled.
23+
"""
24+
25+
TRACEPARENT_VERSION = "00"
26+
"""
27+
W3C Trace Context version identifier.
28+
"""
29+
30+
31+
class MCPEnvironmentVariables:
32+
"""Environment variable names for MCP instrumentation configuration."""
33+
34+
SERVER_NAME = "MCP_INSTRUMENTATION_SERVER_NAME"
35+
"""
36+
Environment variable to override the default MCP server name.
37+
Default value: "mcp server"
38+
"""

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
# SPDX-License-Identifier: Apache-2.0
33
from typing import Any, Callable, Collection, Dict, Tuple
44

5-
from mcp import ClientRequest
65
from wrapt import register_post_import_hook, wrap_function_wrapper
76

87
from opentelemetry import trace
98
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
109
from opentelemetry.instrumentation.utils import unwrap
11-
from .semconv import MCPAttributes, MCPSpanNames, MCPOperations, MCPTraceContext, MCPEnvironmentVariables
12-
_instruments = ("mcp >= 1.6.0",)
10+
from opentelemetry.semconv.trace import SpanAttributes
11+
12+
from .constants import MCPEnvironmentVariables, MCPTraceContext
13+
from .semconv import MCPAttributes, MCPOperations, MCPSpanNames
14+
1315

1416
class MCPInstrumentor(BaseInstrumentor):
1517
"""
@@ -22,7 +24,7 @@ def __init__(self):
2224

2325
@staticmethod
2426
def instrumentation_dependencies() -> Collection[str]:
25-
return _instruments
27+
return ("mcp >= 1.6.0",)
2628

2729
def _instrument(self, **kwargs: Any) -> None:
2830
tracer_provider = kwargs.get("tracer_provider")
@@ -65,7 +67,9 @@ def _wrap_send_request(
6567
"""
6668

6769
async def async_wrapper():
68-
with self.tracer.start_as_current_span(MCPSpanNames.CLIENT_SEND_REQUEST, kind=trace.SpanKind.CLIENT) as span:
70+
with self.tracer.start_as_current_span(
71+
MCPSpanNames.CLIENT_SEND_REQUEST, kind=trace.SpanKind.CLIENT
72+
) as span:
6973
span_ctx = span.get_span_context()
7074
request = args[0] if len(args) > 0 else kwargs.get("request")
7175
if request:
@@ -122,11 +126,12 @@ async def _wrap_handle_request(
122126
else:
123127
return await wrapped(*args, **kwargs)
124128

125-
def _generate_mcp_attributes(self, span: trace.Span, request: ClientRequest, is_client: bool) -> None:
129+
@staticmethod
130+
def _generate_mcp_attributes(span: trace.Span, request: Any, is_client: bool) -> None:
126131
import mcp.types as types # pylint: disable=import-outside-toplevel,consider-using-from-import
127132

128133
operation = MCPOperations.UNKNOWN_OPERATION
129-
134+
130135
if isinstance(request, types.ListToolsRequest):
131136
operation = MCPOperations.LIST_TOOL
132137
span.set_attribute(MCPAttributes.MCP_LIST_TOOLS, True)
@@ -142,11 +147,11 @@ def _generate_mcp_attributes(self, span: trace.Span, request: ClientRequest, is_
142147
span.set_attribute(MCPAttributes.MCP_INITIALIZE, True)
143148
if is_client:
144149
span.update_name(MCPSpanNames.CLIENT_INITIALIZE)
145-
150+
146151
if is_client:
147-
self._add_client_attributes(span, operation, request)
152+
MCPInstrumentor._add_client_attributes(span, operation, request)
148153
else:
149-
self._add_server_attributes(span, operation, request)
154+
MCPInstrumentor._add_server_attributes(span, operation, request)
150155

151156
@staticmethod
152157
def _inject_trace_context(request_data: Dict[str, Any], span_ctx) -> None:
@@ -179,7 +184,8 @@ def _extract_span_context_from_traceparent(traceparent: str):
179184
return None
180185

181186
@staticmethod
182-
def _get_mcp_operation(req: ClientRequest) -> str:
187+
def _get_mcp_operation(req: Any) -> str:
188+
183189
import mcp.types as types # pylint: disable=import-outside-toplevel,consider-using-from-import
184190

185191
span_name = "unknown"
@@ -188,21 +194,19 @@ def _get_mcp_operation(req: ClientRequest) -> str:
188194
span_name = MCPSpanNames.TOOLS_LIST
189195
elif isinstance(req, types.CallToolRequest):
190196
span_name = MCPSpanNames.tools_call(req.params.name)
191-
elif isinstance(req, types.InitializeRequest):
192-
span_name = MCPSpanNames.TOOLS_INITIALIZE
193197
return span_name
194198

195199
@staticmethod
196-
def _add_client_attributes(span: trace.Span, operation: str, request: ClientRequest) -> None:
200+
def _add_client_attributes(span: trace.Span, operation: str, request: Any) -> None:
197201
import os # pylint: disable=import-outside-toplevel
198202

199203
service_name = os.environ.get(MCPEnvironmentVariables.SERVER_NAME, "mcp server")
200-
span.set_attribute(MCPAttributes.AWS_REMOTE_SERVICE, service_name)
201-
span.set_attribute(MCPAttributes.AWS_REMOTE_OPERATION, operation)
204+
span.set_attribute(SpanAttributes.RPC_SERVICE, service_name)
205+
span.set_attribute(SpanAttributes.RPC_METHOD, operation)
202206
if hasattr(request, "params") and request.params and hasattr(request.params, "name"):
203207
span.set_attribute(MCPAttributes.MCP_TOOL_NAME, request.params.name)
204208

205209
@staticmethod
206-
def _add_server_attributes(span: trace.Span, operation: str, request: ClientRequest) -> None:
210+
def _add_server_attributes(span: trace.Span, operation: str, request: Any) -> None:
207211
if hasattr(request, "params") and request.params and hasattr(request.params, "name"):
208-
span.set_attribute(MCPAttributes.MCP_TOOL_NAME, 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

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class MCPAttributes:
1313
"""MCP-specific span attributes for OpenTelemetry instrumentation."""
1414

1515
# MCP Operation Type Attributes
16-
MCP_INITIALIZE = "mcp.initialize"
16+
MCP_INITIALIZE = "notifications/initialize"
1717
"""
1818
Boolean attribute indicating this span represents an MCP initialize operation.
1919
Set to True when the span tracks session initialization between client and server.
@@ -38,19 +38,6 @@ class MCPAttributes:
3838
Example: "echo", "search", "calculator"
3939
"""
4040

41-
# AWS-specific Remote Service Attributes
42-
AWS_REMOTE_SERVICE = "aws.remote.service"
43-
"""
44-
The name of the remote MCP service being called.
45-
Default: "mcp server" (can be overridden via MCP_INSTRUMENTATION_SERVER_NAME env var)
46-
"""
47-
48-
AWS_REMOTE_OPERATION = "aws.remote.operation"
49-
"""
50-
The specific MCP operation being performed.
51-
Values: "Initialize", "ListTool", or the specific tool name for call operations
52-
"""
53-
5441

5542
class MCPSpanNames:
5643
"""Standard span names for MCP operations."""
@@ -62,7 +49,7 @@ class MCPSpanNames:
6249
Used for all outgoing MCP requests (initialize, list tools, call tool).
6350
"""
6451

65-
CLIENT_INITIALIZE = "mcp.initialize"
52+
CLIENT_INITIALIZE = "notifications/initialize"
6653
"""
6754
Span name for client-side MCP initialization requests.
6855
"""
@@ -85,13 +72,6 @@ def client_call_tool(tool_name: str) -> str:
8572
"""
8673
return f"mcp.call_tool.{tool_name}"
8774

88-
# Server-side span names
89-
TOOLS_INITIALIZE = "tools/initialize"
90-
"""
91-
Span name for server-side MCP initialization handling.
92-
Tracks server processing of client initialization requests.
93-
"""
94-
9575
TOOLS_LIST = "tools/list"
9676
"""
9777
Span name for server-side MCP list tools handling.
@@ -115,41 +95,11 @@ def tools_call(tool_name: str) -> str:
11595
class MCPOperations:
11696
"""Standard operation names for MCP semantic conventions."""
11797

118-
INITIALIZE = "Initialize"
98+
INITIALIZE = "Notifications/Initialize"
11999
"""Operation name for MCP session initialization."""
120100

121101
LIST_TOOL = "ListTool"
122102
"""Operation name for MCP tool discovery."""
123103

124104
UNKNOWN_OPERATION = "UnknownOperation"
125105
"""Fallback operation name for unrecognized MCP operations."""
126-
127-
128-
class MCPTraceContext:
129-
"""Constants for MCP distributed tracing context propagation."""
130-
131-
TRACEPARENT_HEADER = "traceparent"
132-
"""
133-
W3C Trace Context traceparent header name.
134-
Used for propagating trace context in MCP request metadata.
135-
"""
136-
137-
TRACE_FLAGS_SAMPLED = "01"
138-
"""
139-
W3C Trace Context flags indicating the trace is sampled.
140-
"""
141-
142-
TRACEPARENT_VERSION = "00"
143-
"""
144-
W3C Trace Context version identifier.
145-
"""
146-
147-
148-
class MCPEnvironmentVariables:
149-
"""Environment variable names for MCP instrumentation configuration."""
150-
151-
SERVER_NAME = "MCP_INSTRUMENTATION_SERVER_NAME"
152-
"""
153-
Environment variable to override the default MCP server name.
154-
Default value: "mcp server"
155-
"""

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_mcpinstrumentor.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def test_instrument_without_tracer_provider_kwargs(self) -> None:
105105
# Verify - tracer should be set from trace.get_tracer
106106
self.assertTrue(hasattr(self.instrumentor, "tracer"))
107107
self.assertEqual(self.instrumentor.tracer, "default_tracer")
108-
mock_get_tracer.assert_called_with("mcp")
108+
mock_get_tracer.assert_called_with("instrumentation.mcp")
109109

110110
def test_instrument_with_tracer_provider_kwargs(self) -> None:
111111
"""Test _instrument method when tracer_provider is in kwargs - should use provider's tracer"""
@@ -122,7 +122,7 @@ def test_instrument_with_tracer_provider_kwargs(self) -> None:
122122
self.assertTrue(hasattr(self.instrumentor, "tracer"))
123123
self.assertEqual(self.instrumentor.tracer, "mock_tracer_from_provider")
124124
self.assertTrue(provider.get_tracer_called)
125-
self.assertEqual(provider.tracer_name, "mcp")
125+
self.assertEqual(provider.tracer_name, "instrumentation.mcp")
126126

127127

128128
class TestInstrumentationDependencies(unittest.TestCase):
@@ -249,7 +249,7 @@ def __init__(self, name: str) -> None:
249249

250250
# Test server handling without trace context (fallback scenario)
251251
with unittest.mock.patch("opentelemetry.trace.get_tracer", return_value=mock_tracer), unittest.mock.patch.dict(
252-
"sys.modules", {"mcp.types": MagicMock()}
252+
"sys.modules", {"mcp.types": MagicMock(), "mcp": MagicMock()}
253253
), unittest.mock.patch.object(self.instrumentor, "_generate_mcp_attributes"), unittest.mock.patch.object(
254254
self.instrumentor, "_get_mcp_operation", return_value="tools/create_metric"
255255
):
@@ -384,7 +384,7 @@ async def server_handle_request(self, session: Any, server_request: Any) -> Dict
384384

385385
# STEP 1: Client sends request through instrumentation
386386
with unittest.mock.patch("opentelemetry.trace.get_tracer", return_value=mock_tracer), unittest.mock.patch.dict(
387-
"sys.modules", {"mcp.types": MagicMock()}
387+
"sys.modules", {"mcp.types": MagicMock(), "mcp": MagicMock()}
388388
), unittest.mock.patch.object(self.instrumentor, "_generate_mcp_attributes"):
389389
# Override the setup tracer with the properly mocked one
390390
self.instrumentor.tracer = mock_tracer
@@ -420,7 +420,7 @@ async def server_handle_request(self, session: Any, server_request: Any) -> Dict
420420

421421
# Server processes the request it received
422422
with unittest.mock.patch("opentelemetry.trace.get_tracer", return_value=mock_tracer), unittest.mock.patch.dict(
423-
"sys.modules", {"mcp.types": MagicMock()}
423+
"sys.modules", {"mcp.types": MagicMock(), "mcp": MagicMock()}
424424
), unittest.mock.patch.object(self.instrumentor, "_generate_mcp_attributes"), unittest.mock.patch.object(
425425
self.instrumentor, "_get_mcp_operation", return_value="tools/create_metric"
426426
):
@@ -465,7 +465,3 @@ async def server_handle_request(self, session: Any, server_request: Any) -> Dict
465465
any(expected_entry in log_entry for log_entry in e2e_system.communication_log),
466466
f"Expected log entry '{expected_entry}' not found in: {e2e_system.communication_log}",
467467
)
468-
469-
470-
if __name__ == "__main__":
471-
unittest.main()

0 commit comments

Comments
 (0)