Skip to content

Commit 746b964

Browse files
authored
Add logfire.instrument_mcp() method (#966)
1 parent b2996f1 commit 746b964

File tree

6 files changed

+339
-23
lines changed

6 files changed

+339
-23
lines changed

logfire-api/logfire_api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ def instrument_aiohttp_client(self, *args, **kwargs) -> None: ...
147147

148148
def instrument_system_metrics(self, *args, **kwargs) -> None: ...
149149

150+
def instrument_mcp(self, *args, **kwargs) -> None: ...
151+
150152
def shutdown(self, *args, **kwargs) -> None: ...
151153

152154
DEFAULT_LOGFIRE_INSTANCE = Logfire()
@@ -191,6 +193,7 @@ def shutdown(self, *args, **kwargs) -> None: ...
191193
instrument_pymongo = DEFAULT_LOGFIRE_INSTANCE.instrument_pymongo
192194
instrument_mysql = DEFAULT_LOGFIRE_INSTANCE.instrument_mysql
193195
instrument_system_metrics = DEFAULT_LOGFIRE_INSTANCE.instrument_system_metrics
196+
instrument_mcp = DEFAULT_LOGFIRE_INSTANCE.instrument_mcp
194197
shutdown = DEFAULT_LOGFIRE_INSTANCE.shutdown
195198
suppress_scopes = DEFAULT_LOGFIRE_INSTANCE.suppress_scopes
196199

logfire/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
instrument_pymongo = DEFAULT_LOGFIRE_INSTANCE.instrument_pymongo
5050
instrument_mysql = DEFAULT_LOGFIRE_INSTANCE.instrument_mysql
5151
instrument_system_metrics = DEFAULT_LOGFIRE_INSTANCE.instrument_system_metrics
52+
instrument_mcp = DEFAULT_LOGFIRE_INSTANCE.instrument_mcp
5253
suppress_scopes = DEFAULT_LOGFIRE_INSTANCE.suppress_scopes
5354
shutdown = DEFAULT_LOGFIRE_INSTANCE.shutdown
5455
with_tags = DEFAULT_LOGFIRE_INSTANCE.with_tags
@@ -138,6 +139,7 @@ def loguru_handler() -> Any:
138139
'instrument_pymongo',
139140
'instrument_mysql',
140141
'instrument_system_metrics',
142+
'instrument_mcp',
141143
'AutoTraceModule',
142144
'with_tags',
143145
'with_settings',
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
from typing import TYPE_CHECKING, Any
5+
6+
from mcp.shared.session import BaseSession, SendRequestT
7+
from mcp.types import CallToolRequest
8+
9+
if TYPE_CHECKING:
10+
from logfire import Logfire
11+
12+
13+
def instrument_mcp(logfire_instance: Logfire):
14+
logfire_instance = logfire_instance.with_settings(custom_scope_suffix='mcp')
15+
16+
original = BaseSession.send_request # type: ignore
17+
18+
@functools.wraps(original) # type: ignore
19+
async def send_request(self, request: SendRequestT, *args, **kwargs: Any): # type: ignore
20+
attributes: dict[str, Any] = {
21+
'request': request,
22+
# https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/
23+
'rpc.system': 'jsonrpc',
24+
'rpc.jsonrpc.version': '2.0',
25+
}
26+
span_name = 'MCP request'
27+
28+
root = request.root
29+
# method should always exist, but it's had to verify because the request type is a RootModel
30+
# of a big union, instead of just using a base class with a method attribute.
31+
if method := getattr(root, 'method', None): # pragma: no branch
32+
span_name += f': {method}'
33+
attributes['rpc.method'] = method
34+
if isinstance(root, CallToolRequest):
35+
span_name += f' {root.params.name}'
36+
37+
with logfire_instance.span(span_name, **attributes) as span:
38+
result = await original(self, request, *args, **kwargs) # type: ignore
39+
span.set_attribute('response', result)
40+
return result # type: ignore
41+
42+
BaseSession.send_request = send_request

logfire/_internal/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@ def install_auto_tracing(
899899
def _warn_if_not_initialized_for_instrumentation(self):
900900
self.config.warn_if_not_initialized('Instrumentation will have no effect')
901901

902+
def instrument_mcp(self) -> None:
903+
"""Instrument [MCP](https://modelcontextprotocol.io/) requests such as tool calls."""
904+
from .integrations.mcp import instrument_mcp
905+
906+
self._warn_if_not_initialized_for_instrumentation()
907+
instrument_mcp(self)
908+
902909
def instrument_pydantic(
903910
self,
904911
record: PydanticPluginRecordValues = 'all',

0 commit comments

Comments
 (0)