Skip to content

Commit 526d9ac

Browse files
committed
OpenTelemetry _meta propagation
1 parent b33c811 commit 526d9ac

File tree

4 files changed

+544
-102
lines changed

4 files changed

+544
-102
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
"pyjwt[crypto]>=2.10.1",
4141
"typing-extensions>=4.13.0",
4242
"typing-inspection>=0.4.1",
43+
"opentelemetry-api>=1.23.0",
4344
]
4445

4546
[project.optional-dependencies]
@@ -71,6 +72,7 @@ dev = [
7172
"coverage[toml]>=7.10.7,<=7.13",
7273
"pillow>=12.0",
7374
"strict-no-cover",
75+
"opentelemetry-sdk>=1.24.0",
7476
]
7577
docs = [
7678
"mkdocs>=1.6.1",

src/mcp/shared/session.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import anyio
1010
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
11+
from opentelemetry.propagate import inject
1112
from pydantic import BaseModel, TypeAdapter
1213
from typing_extensions import Self
1314

@@ -263,6 +264,9 @@ async def send_request(
263264
# Store the callback for this request
264265
self._progress_callbacks[request_id] = progress_callback
265266

267+
# Propagate opentelemetry trace context
268+
self._inject_otel_context(request_data)
269+
266270
try:
267271
jsonrpc_request = JSONRPCRequest(jsonrpc="2.0", id=request_id, **request_data)
268272
await self._write_stream.send(SessionMessage(message=jsonrpc_request, metadata=metadata))
@@ -295,18 +299,37 @@ async def send_notification(
295299
related_request_id: RequestId | None = None,
296300
) -> None:
297301
"""Emits a notification, which is a one-way message that does not expect a response."""
302+
303+
request_data = notification.model_dump(by_alias=True, mode="json", exclude_none=True)
304+
# Propagate opentelemetry trace context
305+
self._inject_otel_context(request_data)
306+
jsonrpc_notification = JSONRPCNotification(jsonrpc="2.0", **request_data)
307+
298308
# Some transport implementations may need to set the related_request_id
299309
# to attribute to the notifications to the request that triggered them.
300-
jsonrpc_notification = JSONRPCNotification(
301-
jsonrpc="2.0",
302-
**notification.model_dump(by_alias=True, mode="json", exclude_none=True),
303-
)
304310
session_message = SessionMessage(
305311
message=jsonrpc_notification,
306312
metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None,
307313
)
308314
await self._write_stream.send(session_message)
309315

316+
def _inject_otel_context(self, request: dict[str, Any]) -> None:
317+
"""Propagate OpenTelemetry context in `_meta`.
318+
319+
See
320+
- SEP414 https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414
321+
- OpenTelemetry semantic conventions
322+
https://github.com/open-telemetry/semantic-conventions/blob/v1.39.0/docs/gen-ai/mcp.md
323+
"""
324+
325+
carrier: dict[str, str] = {}
326+
inject(carrier)
327+
if not carrier:
328+
return
329+
330+
meta: dict[str, Any] = request.setdefault("params", {}).setdefault("_meta", {})
331+
meta.update(carrier)
332+
310333
async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
311334
if isinstance(response, ErrorData):
312335
jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)

0 commit comments

Comments
 (0)