|
24 | 24 | except ImportError: |
25 | 25 | raise DidNotEnable("MCP SDK not installed") |
26 | 26 |
|
| 27 | +try: |
| 28 | + from fastmcp import FastMCP |
| 29 | +except ImportError: |
| 30 | + FastMCP = None |
| 31 | + |
27 | 32 |
|
28 | 33 | if TYPE_CHECKING: |
29 | 34 | from typing import Any, Callable, Optional |
@@ -52,6 +57,9 @@ def setup_once(): |
52 | 57 | """ |
53 | 58 | _patch_lowlevel_server() |
54 | 59 |
|
| 60 | + if FastMCP is not None: |
| 61 | + _patch_fastmcp() |
| 62 | + |
55 | 63 |
|
56 | 64 | def _get_request_context_data(): |
57 | 65 | # type: () -> tuple[Optional[str], Optional[str], str] |
@@ -564,3 +572,100 @@ def patched_read_resource(self): |
564 | 572 | )(func) |
565 | 573 |
|
566 | 574 | 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