Skip to content

Commit bf29c56

Browse files
committed
Patch FastMCP
1 parent 69cdc00 commit bf29c56

File tree

1 file changed

+105
-0
lines changed

1 file changed

+105
-0
lines changed

sentry_sdk/integrations/mcp.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
except ImportError:
2525
raise DidNotEnable("MCP SDK not installed")
2626

27+
try:
28+
from fastmcp import FastMCP
29+
except ImportError:
30+
FastMCP = None
31+
2732

2833
if TYPE_CHECKING:
2934
from typing import Any, Callable, Optional
@@ -52,6 +57,9 @@ def setup_once():
5257
"""
5358
_patch_lowlevel_server()
5459

60+
if FastMCP is not None:
61+
_patch_fastmcp()
62+
5563

5664
def _get_request_context_data():
5765
# type: () -> tuple[Optional[str], Optional[str], str]
@@ -564,3 +572,100 @@ def patched_read_resource(self):
564572
)(func)
565573

566574
Server.read_resource = patched_read_resource
575+
576+
577+
def _patch_fastmcp():
578+
# type: () -> None
579+
"""
580+
Patches the standalone fastmcp package's FastMCP class.
581+
582+
The standalone fastmcp package (v2.14.0+) registers its own handlers for
583+
prompts and resources directly, bypassing the Server decorators we patch.
584+
This function patches the _get_prompt_mcp and _read_resource_mcp methods
585+
to add instrumentation for those handlers.
586+
"""
587+
if hasattr(FastMCP, "_get_prompt_mcp"):
588+
original_get_prompt_mcp = FastMCP._get_prompt_mcp
589+
590+
@wraps(original_get_prompt_mcp)
591+
async def patched_get_prompt_mcp(self, name, arguments=None):
592+
# type: (Any, str, Optional[dict[str, Any]]) -> Any
593+
return await _async_fastmcp_handler_wrapper(
594+
"prompt",
595+
lambda n, a: original_get_prompt_mcp(self, n, a),
596+
(name, arguments),
597+
)
598+
599+
FastMCP._get_prompt_mcp = patched_get_prompt_mcp
600+
601+
# Patch _read_resource_mcp
602+
if hasattr(FastMCP, "_read_resource_mcp"):
603+
original_read_resource_mcp = FastMCP._read_resource_mcp
604+
605+
@wraps(original_read_resource_mcp)
606+
async def patched_read_resource_mcp(self, uri):
607+
# type: (Any, Any) -> Any
608+
return await _async_fastmcp_handler_wrapper(
609+
"resource",
610+
lambda u: original_read_resource_mcp(self, u),
611+
(uri,),
612+
)
613+
614+
FastMCP._read_resource_mcp = patched_read_resource_mcp
615+
616+
617+
async def _async_fastmcp_handler_wrapper(handler_type, func, original_args):
618+
# type: (str, Callable[..., Any], tuple[Any, ...]) -> Any
619+
"""
620+
Async wrapper for standalone FastMCP handlers.
621+
622+
Similar to _async_handler_wrapper but the original function is already
623+
a coroutine function that we call directly.
624+
"""
625+
(
626+
handler_name,
627+
arguments,
628+
span_data_key,
629+
span_name,
630+
mcp_method_name,
631+
result_data_key,
632+
) = _prepare_handler_data(handler_type, original_args)
633+
634+
with get_start_span_function()(
635+
op=OP.MCP_SERVER,
636+
name=span_name,
637+
origin=MCPIntegration.origin,
638+
) as span:
639+
request_id, session_id, mcp_transport = _get_request_context_data()
640+
641+
_set_span_input_data(
642+
span,
643+
handler_name,
644+
span_data_key,
645+
mcp_method_name,
646+
arguments,
647+
request_id,
648+
session_id,
649+
mcp_transport,
650+
)
651+
652+
if handler_type == "resource":
653+
uri = original_args[0]
654+
protocol = None
655+
if hasattr(uri, "scheme"):
656+
protocol = uri.scheme
657+
elif handler_name and "://" in handler_name:
658+
protocol = handler_name.split("://")[0]
659+
if protocol:
660+
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
661+
662+
try:
663+
result = await func(*original_args)
664+
except Exception as e:
665+
if handler_type == "tool":
666+
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
667+
sentry_sdk.capture_exception(e)
668+
raise
669+
670+
_set_span_output_data(span, result, result_data_key, handler_type)
671+
return result

0 commit comments

Comments
 (0)