Skip to content

Commit 61ea9e0

Browse files
authored
feat(mcp): Patch FastMCP's resource, prompt handlers (#5233)
Adapt to new FastMCP release that broke our integration. FastMCP v2.14.0+ registers its own handlers for prompts and resources directly, bypassing the Server decorators we patch.
1 parent 6253f0c commit 61ea9e0

File tree

1 file changed

+106
-11
lines changed

1 file changed

+106
-11
lines changed

sentry_sdk/integrations/mcp.py

Lines changed: 106 additions & 11 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 # type: ignore[import-not-found]
29+
except ImportError:
30+
FastMCP = None
31+
2732

2833
if TYPE_CHECKING:
2934
from typing import Any, Callable, Optional
@@ -50,6 +55,9 @@ def setup_once() -> None:
5055
"""
5156
_patch_lowlevel_server()
5257

58+
if FastMCP is not None:
59+
_patch_fastmcp()
60+
5361

5462
def _get_request_context_data() -> "tuple[Optional[str], Optional[str], str]":
5563
"""
@@ -280,26 +288,53 @@ def _set_span_output_data(
280288

281289

282290
def _prepare_handler_data(
283-
handler_type: str, original_args: "tuple[Any, ...]"
291+
handler_type: str,
292+
original_args: "tuple[Any, ...]",
293+
original_kwargs: "Optional[dict[str, Any]]" = None,
284294
) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]":
285295
"""
286296
Prepare common handler data for both async and sync wrappers.
287297
288298
Returns:
289299
Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key)
290300
"""
301+
original_kwargs = original_kwargs or {}
302+
291303
# Extract handler-specific data based on handler type
292304
if handler_type == "tool":
293-
handler_name = original_args[0] # tool_name
294-
arguments = original_args[1] if len(original_args) > 1 else {}
305+
if original_args:
306+
handler_name = original_args[0]
307+
elif original_kwargs.get("name"):
308+
handler_name = original_kwargs["name"]
309+
310+
arguments = {}
311+
if len(original_args) > 1:
312+
arguments = original_args[1]
313+
elif original_kwargs.get("arguments"):
314+
arguments = original_kwargs["arguments"]
315+
295316
elif handler_type == "prompt":
296-
handler_name = original_args[0] # name
297-
arguments = original_args[1] if len(original_args) > 1 else {}
317+
if original_args:
318+
handler_name = original_args[0]
319+
elif original_kwargs.get("name"):
320+
handler_name = original_kwargs["name"]
321+
322+
arguments = {}
323+
if len(original_args) > 1:
324+
arguments = original_args[1]
325+
elif original_kwargs.get("arguments"):
326+
arguments = original_kwargs["arguments"]
327+
298328
# Include name in arguments dict for span data
299329
arguments = {"name": handler_name, **(arguments or {})}
330+
300331
else: # resource
301-
uri = original_args[0]
302-
handler_name = str(uri) if uri else "unknown"
332+
handler_name = "unknown"
333+
if original_args:
334+
handler_name = str(original_args[0])
335+
elif original_kwargs.get("uri"):
336+
handler_name = str(original_kwargs["uri"])
337+
303338
arguments = {}
304339

305340
# Get span configuration
@@ -318,7 +353,11 @@ def _prepare_handler_data(
318353

319354

320355
async def _async_handler_wrapper(
321-
handler_type: str, func: "Callable[..., Any]", original_args: "tuple[Any, ...]"
356+
handler_type: str,
357+
func: "Callable[..., Any]",
358+
original_args: "tuple[Any, ...]",
359+
original_kwargs: "Optional[dict[str, Any]]" = None,
360+
self: "Optional[Any]" = None,
322361
) -> "Any":
323362
"""
324363
Async wrapper for MCP handlers.
@@ -327,15 +366,20 @@ async def _async_handler_wrapper(
327366
handler_type: "tool", "prompt", or "resource"
328367
func: The async handler function to wrap
329368
original_args: Original arguments passed to the handler
369+
original_kwargs: Original keyword arguments passed to the handler
370+
self: Optional instance for bound methods
330371
"""
372+
if original_kwargs is None:
373+
original_kwargs = {}
374+
331375
(
332376
handler_name,
333377
arguments,
334378
span_data_key,
335379
span_name,
336380
mcp_method_name,
337381
result_data_key,
338-
) = _prepare_handler_data(handler_type, original_args)
382+
) = _prepare_handler_data(handler_type, original_args, original_kwargs)
339383

340384
# Start span and execute
341385
with get_start_span_function()(
@@ -360,7 +404,11 @@ async def _async_handler_wrapper(
360404

361405
# For resources, extract and set protocol
362406
if handler_type == "resource":
363-
uri = original_args[0]
407+
if original_args:
408+
uri = original_args[0]
409+
else:
410+
uri = original_kwargs.get("uri")
411+
364412
protocol = None
365413
if hasattr(uri, "scheme"):
366414
protocol = uri.scheme
@@ -371,7 +419,9 @@ async def _async_handler_wrapper(
371419

372420
try:
373421
# Execute the async handler
374-
result = await func(*original_args)
422+
if self is not None:
423+
original_args = (self, *original_args)
424+
result = await func(*original_args, **original_kwargs)
375425
except Exception as e:
376426
# Set error flag for tools
377427
if handler_type == "tool":
@@ -566,3 +616,48 @@ def patched_read_resource(
566616
)(func)
567617

568618
Server.read_resource = patched_read_resource
619+
620+
621+
def _patch_fastmcp():
622+
# type: () -> None
623+
"""
624+
Patches the standalone fastmcp package's FastMCP class.
625+
626+
The standalone fastmcp package (v2.14.0+) registers its own handlers for
627+
prompts and resources directly, bypassing the Server decorators we patch.
628+
This function patches the _get_prompt_mcp and _read_resource_mcp methods
629+
to add instrumentation for those handlers.
630+
"""
631+
if hasattr(FastMCP, "_get_prompt_mcp"):
632+
original_get_prompt_mcp = FastMCP._get_prompt_mcp
633+
634+
@wraps(original_get_prompt_mcp)
635+
async def patched_get_prompt_mcp(
636+
self: "Any", *args: "Any", **kwargs: "Any"
637+
) -> "Any":
638+
return await _async_handler_wrapper(
639+
"prompt",
640+
original_get_prompt_mcp,
641+
args,
642+
kwargs,
643+
self,
644+
)
645+
646+
FastMCP._get_prompt_mcp = patched_get_prompt_mcp
647+
648+
if hasattr(FastMCP, "_read_resource_mcp"):
649+
original_read_resource_mcp = FastMCP._read_resource_mcp
650+
651+
@wraps(original_read_resource_mcp)
652+
async def patched_read_resource_mcp(
653+
self: "Any", *args: "Any", **kwargs: "Any"
654+
) -> "Any":
655+
return await _async_handler_wrapper(
656+
"resource",
657+
original_read_resource_mcp,
658+
args,
659+
kwargs,
660+
self,
661+
)
662+
663+
FastMCP._read_resource_mcp = patched_read_resource_mcp

0 commit comments

Comments
 (0)