From 360401b08d4578f9bef542375925bb4c12948545 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 14:49:45 +0100 Subject: [PATCH 1/8] test(fastmcp): Ensure MCP spans are children of HTTP transactions --- tests/conftest.py | 80 ++++++++ tests/integrations/fastmcp/test_fastmcp.py | 210 +++++++++++++++------ 2 files changed, 228 insertions(+), 62 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index dea36f8bda..b208c97c3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ from werkzeug.wrappers import Request, Response import jsonschema +from starlette.testclient import TestClient try: import gevent @@ -592,6 +593,85 @@ def suppress_deprecation_warnings(): yield +@pytest.fixture() +def json_rpc(): + def inner(app, method: str, params, request_id: str | None = None): + if request_id is None: + request_id = "1" # arbitrary + + with TestClient(app) as client: + init_response = client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + }, + json={ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "clientInfo": {"name": "test-client", "version": "1.0"}, + "protocolVersion": "2025-11-25", + "capabilities": {}, + }, + "id": request_id, + }, + ) + + session_id = init_response.headers["mcp-session-id"] + + # Notification response is mandatory. + # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle + client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + "mcp-session-id": session_id, + }, + json={ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {}, + }, + ) + + response = client.post( + "/mcp/", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + "mcp-session-id": session_id, + }, + json={ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": request_id, + }, + ) + + return session_id, response + + return inner + + +@pytest.fixture() +def select_transactions_with_mcp_spans(): + def inner(events, method_name): + return [ + transaction + for transaction in events + if transaction["type"] == "transaction" + and any( + span["data"].get("mcp.method.name") == method_name + for span in transaction.get("spans", []) + ) + ] + + return inner + + class MockServerRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 # Process an HTTP GET request and return a response. diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 4f2d0e6916..150a10cf66 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -39,6 +39,11 @@ async def __call__(self, *args, **kwargs): from sentry_sdk.consts import SPANDATA, OP from sentry_sdk.integrations.mcp import MCPIntegration +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager + +from starlette.routing import Mount +from starlette.applications import Starlette + # Try to import both FastMCP implementations try: from mcp.server.fastmcp import FastMCP as MCPFastMCP @@ -341,7 +346,13 @@ def add_numbers(a: int, b: int) -> dict: [(True, True), (True, False), (False, True), (False, False)], ) async def test_fastmcp_tool_async( - sentry_init, capture_events, FastMCP, send_default_pii, include_prompts + sentry_init, + capture_events, + FastMCP, + send_default_pii, + include_prompts, + json_rpc, + select_transactions_with_mcp_spans, ): """Test that FastMCP async tool handlers create proper spans""" sentry_init( @@ -353,28 +364,47 @@ async def test_fastmcp_tool_async( mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-456", session_id="session-789", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) @mcp.tool() async def multiply_numbers(x: int, y: int) -> dict: """Multiply two numbers together""" return {"result": x * y, "operation": "multiplication"} - with start_transaction(name="fastmcp tx"): - result = await call_tool_through_mcp_async( - mcp, "multiply_numbers", {"x": 7, "y": 6} + session_id, result = json_rpc( + app, + method="tools/call", + params={ + "name": "multiply_numbers", + "arguments": {"x": 7, "y": 6}, + }, + request_id="req-456", + ) + + if isinstance(mcp, StandaloneFastMCP): + assert result.json()["result"]["structuredContent"] == { + "result": 42, + "operation": "multiplication", + } + else: + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"result": 42, "operation": "multiplication"}, + indent=2, ) - assert result == {"result": 42, "operation": "multiplication"} - - (tx,) = events - assert tx["type"] == "transaction" - assert len(tx["spans"]) == 1 + transactions = select_transactions_with_mcp_spans(events, "tools/call") + assert len(transactions) == 1 + tx = transactions[0] # Verify span structure span = tx["spans"][0] @@ -385,7 +415,7 @@ async def multiply_numbers(x: int, y: int) -> dict: assert span["data"][SPANDATA.MCP_METHOD_NAME] == "tools/call" assert span["data"][SPANDATA.MCP_TRANSPORT] == "http" assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" - assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-789" + assert span["data"][SPANDATA.MCP_SESSION_ID] == session_id # Check PII-sensitive data if send_default_pii and include_prompts: @@ -617,7 +647,13 @@ def code_help_prompt(language: str): @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_prompt_async(sentry_init, capture_events, FastMCP): +async def test_fastmcp_prompt_async( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_transactions_with_mcp_spans, +): """Test that FastMCP async prompt handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -627,12 +663,17 @@ async def test_fastmcp_prompt_async(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-async-prompt", session_id="session-abc", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) # Try to register an async prompt handler try: @@ -655,15 +696,19 @@ async def async_prompt(topic: str): }, ] - with start_transaction(name="fastmcp tx"): - result = await call_prompt_through_mcp_async( - mcp, "async_prompt", {"topic": "MCP"} - ) + _, result = json_rpc( + app, + method="prompts/get", + params={ + "name": "async_prompt", + "arguments": {"topic": "MCP"}, + }, + ) - assert len(result.messages) == 2 + assert len(result.json()["result"]["messages"]) == 2 - (tx,) = events - assert tx["type"] == "transaction" + transactions = select_transactions_with_mcp_spans(events, "prompts/get") + assert len(transactions) == 1 except AttributeError: # Prompt handler not supported in this version pytest.skip("Prompt handlers not supported in this FastMCP version") @@ -730,7 +775,13 @@ def read_file(path: str): @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_resource_async(sentry_init, capture_events, FastMCP): +async def test_fastmcp_resource_async( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_transactions_with_mcp_spans, +): """Test that FastMCP async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -740,12 +791,17 @@ async def test_fastmcp_resource_async(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-async-resource", session_id="session-res", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) # Try to register an async resource handler try: @@ -756,23 +812,27 @@ async def read_url(resource: str): """Read a URL resource""" return "resource data" - with start_transaction(name="fastmcp tx"): - try: - result = await call_resource_through_mcp_async( - mcp, "https://example.com/resource" + try: + _, result = json_rpc( + app, + method="resources/read", + params={ + "uri": "https://example.com/resource", + }, + ) + except ValueError as e: + # Older FastMCP versions may not support this URI pattern + if "Unknown resource" in str(e): + pytest.skip( + f"Resource URI not supported in this FastMCP version: {e}" ) - except ValueError as e: - # Older FastMCP versions may not support this URI pattern - if "Unknown resource" in str(e): - pytest.skip( - f"Resource URI not supported in this FastMCP version: {e}" - ) - raise + raise - assert "resource data" in result.contents[0].text + assert "resource data" in result.json()["result"]["contents"][0]["text"] - (tx,) = events - assert tx["type"] == "transaction" + transactions = select_transactions_with_mcp_spans(events, "resources/read") + assert len(transactions) == 1 + tx = transactions[0] # Verify span was created resource_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] @@ -896,7 +956,13 @@ def sse_tool(value: str) -> dict: @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_http_transport(sentry_init, capture_events, FastMCP): +def test_fastmcp_http_transport( + sentry_init, + capture_events, + FastMCP, + json_rpc, + select_transactions_with_mcp_spans, +): """Test that FastMCP correctly detects HTTP transport""" sentry_init( integrations=[MCPIntegration()], @@ -906,24 +972,44 @@ def test_fastmcp_http_transport(sentry_init, capture_events, FastMCP): mcp = FastMCP("Test Server") - # Set up mock request context with HTTP transport - if request_ctx is not None: - mock_ctx = MockRequestContext( - request_id="req-http", session_id="session-http-456", transport="http" - ) - request_ctx.set(mock_ctx) + session_manager = StreamableHTTPSessionManager( + app=mcp._mcp_server, + json_response=True, + ) + + app = Starlette( + routes=[ + Mount("/mcp", app=session_manager.handle_request), + ], + lifespan=lambda app: session_manager.run(), + ) @mcp.tool() def http_tool(data: str) -> dict: """Tool for HTTP transport test""" return {"processed": data.upper()} - with start_transaction(name="fastmcp tx"): - result = call_tool_through_mcp(mcp, "http_tool", {"data": "test"}) + _, result = json_rpc( + app, + method="tools/call", + params={ + "name": "http_tool", + "arguments": {"data": "test"}, + }, + ) - assert result == {"processed": "TEST"} + if isinstance(mcp, StandaloneFastMCP): + print("structured") + assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} + else: + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"processed": "TEST"}, + indent=2, + ) - (tx,) = events + transactions = select_transactions_with_mcp_spans(events, "tools/call") + assert len(transactions) == 1 + tx = transactions[0] # Find MCP spans mcp_spans = [s for s in tx["spans"] if s["op"] == OP.MCP_SERVER] From facb0797e7f133d615f8734f245ba5cef2e568f0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 14:53:45 +0100 Subject: [PATCH 2/8] tolerate missing starlette import --- tests/conftest.py | 5 +- tests/integrations/mcp/test_mcp.py | 100 +++++++---------------------- 2 files changed, 28 insertions(+), 77 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b208c97c3c..059a18fafb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,10 @@ from werkzeug.wrappers import Request, Response import jsonschema -from starlette.testclient import TestClient +try: + from starlette.testclient import TestClient +except ImportError: + TestClient = None try: import gevent diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 0c6b95b391..7e05503e53 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -36,7 +36,6 @@ async def __call__(self, *args, **kwargs): from starlette.routing import Mount from starlette.applications import Starlette -from starlette.testclient import TestClient try: from mcp.server.lowlevel.server import request_ctx @@ -48,77 +47,6 @@ async def __call__(self, *args, **kwargs): from sentry_sdk.integrations.mcp import MCPIntegration -def json_rpc(app, method: str, params, request_id: str | None = None): - if request_id is None: - request_id = "1" # arbitrary - - with TestClient(app) as client: - init_response = client.post( - "/mcp/", - headers={ - "Accept": "application/json, text/event-stream", - "Content-Type": "application/json", - }, - json={ - "jsonrpc": "2.0", - "method": "initialize", - "params": { - "clientInfo": {"name": "test-client", "version": "1.0"}, - "protocolVersion": "2025-11-25", - "capabilities": {}, - }, - "id": request_id, - }, - ) - - session_id = init_response.headers["mcp-session-id"] - - # Notification response is mandatory. - # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle - client.post( - "/mcp/", - headers={ - "Accept": "application/json, text/event-stream", - "Content-Type": "application/json", - "mcp-session-id": session_id, - }, - json={ - "jsonrpc": "2.0", - "method": "notifications/initialized", - "params": {}, - }, - ) - - response = client.post( - "/mcp/", - headers={ - "Accept": "application/json, text/event-stream", - "Content-Type": "application/json", - "mcp-session-id": session_id, - }, - json={ - "jsonrpc": "2.0", - "method": method, - "params": params, - "id": request_id, - }, - ) - - return session_id, response - - -def select_transactions_with_mcp_spans(events, method_name): - return [ - transaction - for transaction in events - if transaction["type"] == "transaction" - and any( - span["data"].get("mcp.method.name") == method_name - for span in transaction.get("spans", []) - ) - ] - - @pytest.fixture(autouse=True) def reset_request_ctx(): """Reset request context before and after each test""" @@ -287,7 +215,12 @@ def test_tool(tool_name, arguments): [(True, True), (True, False), (False, True), (False, False)], ) async def test_tool_handler_async( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + json_rpc, + select_transactions_with_mcp_spans, ): """Test that async tool handlers create proper spans""" sentry_init( @@ -476,7 +409,12 @@ def test_prompt(name, arguments): [(True, True), (True, False), (False, True), (False, False)], ) async def test_prompt_handler_async( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + json_rpc, + select_transactions_with_mcp_spans, ): """Test that async prompt handlers create proper spans""" sentry_init( @@ -619,7 +557,12 @@ def test_resource(uri): @pytest.mark.asyncio -async def test_resource_handler_async(sentry_init, capture_events): +async def test_resource_handler_async( + sentry_init, + capture_events, + json_rpc, + select_transactions_with_mcp_spans, +): """Test that async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -1098,7 +1041,12 @@ def test_tool(tool_name, arguments): assert span["data"][SPANDATA.MCP_SESSION_ID] == "session-sse-123" -def test_streamable_http_transport_detection(sentry_init, capture_events): +def test_streamable_http_transport_detection( + sentry_init, + capture_events, + json_rpc, + select_transactions_with_mcp_spans, +): """Test that StreamableHTTP transport is correctly detected via header""" sentry_init( integrations=[MCPIntegration()], From 66c226df0794b082468657aefabaafb83f8be839 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 15:07:41 +0100 Subject: [PATCH 3/8] correct version gating --- tests/integrations/fastmcp/test_fastmcp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 150a10cf66..cb879fd0d1 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -76,6 +76,7 @@ async def __call__(self, *args, **kwargs): GetPromptRequest = None ReadResourceRequest = None +from fastmcp import __version__ as FASTMCP_VERSION # Collect available FastMCP implementations for parametrization fastmcp_implementations = [] @@ -391,7 +392,7 @@ async def multiply_numbers(x: int, y: int) -> dict: request_id="req-456", ) - if isinstance(mcp, StandaloneFastMCP): + if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): assert result.json()["result"]["structuredContent"] == { "result": 42, "operation": "multiplication", @@ -998,7 +999,7 @@ def http_tool(data: str) -> dict: }, ) - if isinstance(mcp, StandaloneFastMCP): + if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): print("structured") assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} else: From f2c788906a687203c24c303cf9aaa5656fda0fdd Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 15:25:57 +0100 Subject: [PATCH 4/8] catch runtime error --- tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 059a18fafb..eb4bb7a078 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,9 @@ try: from starlette.testclient import TestClient -except ImportError: + # Catch RuntimeError to prevent the following exception in aws_lambda tests. + # RuntimeError: The starlette.testclient module requires the httpx package to be installed. +except (ImportError, RuntimeError): TestClient = None try: From 2f98f7a28a432cfb7b6ec722cb2df5d0d1cb82e8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 15:41:04 +0100 Subject: [PATCH 5/8] version gate version 0 of fastmcp --- tests/integrations/fastmcp/test_fastmcp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index cb879fd0d1..71103dd7d8 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -397,6 +397,10 @@ async def multiply_numbers(x: int, y: int) -> dict: "result": 42, "operation": "multiplication", } + elif isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("0"): + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"result": 42, "operation": "multiplication"}, + ) else: assert result.json()["result"]["content"][0]["text"] == json.dumps( {"result": 42, "operation": "multiplication"}, @@ -1000,8 +1004,11 @@ def http_tool(data: str) -> dict: ) if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): - print("structured") assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} + elif isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("0"): + assert result.json()["result"]["content"][0]["text"] == json.dumps( + {"processed": "TEST"}, + ) else: assert result.json()["result"]["content"][0]["text"] == json.dumps( {"processed": "TEST"}, From 2b6dff27e3af77a866bf87eaedf21110565e32b8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 15:47:15 +0100 Subject: [PATCH 6/8] version gating attempt --- tests/integrations/fastmcp/test_fastmcp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 71103dd7d8..4c5335888d 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -397,7 +397,7 @@ async def multiply_numbers(x: int, y: int) -> dict: "result": 42, "operation": "multiplication", } - elif isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("0"): + elif isinstance(mcp, StandaloneFastMCP): assert result.json()["result"]["content"][0]["text"] == json.dumps( {"result": 42, "operation": "multiplication"}, ) @@ -1005,7 +1005,7 @@ def http_tool(data: str) -> dict: if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} - elif isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("0"): + elif isinstance(mcp, StandaloneFastMCP): assert result.json()["result"]["content"][0]["text"] == json.dumps( {"processed": "TEST"}, ) From 9716ec9a5828ccba2a85aab5770ad0f0554d0513 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 15:51:55 +0100 Subject: [PATCH 7/8] handle missing version string --- tests/integrations/fastmcp/test_fastmcp.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 4c5335888d..a9d6306344 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -76,7 +76,10 @@ async def __call__(self, *args, **kwargs): GetPromptRequest = None ReadResourceRequest = None -from fastmcp import __version__ as FASTMCP_VERSION +try: + from fastmcp import __version__ as FASTMCP_VERSION +except ImportError: + FASTMCP_VERSION = None # Collect available FastMCP implementations for parametrization fastmcp_implementations = [] @@ -392,7 +395,11 @@ async def multiply_numbers(x: int, y: int) -> dict: request_id="req-456", ) - if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): + if ( + isinstance(mcp, StandaloneFastMCP) + and FASTMCP_VERSION is not None + and FASTMCP_VERSION.startswith("2") + ): assert result.json()["result"]["structuredContent"] == { "result": 42, "operation": "multiplication", @@ -1003,7 +1010,11 @@ def http_tool(data: str) -> dict: }, ) - if isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION.startswith("2"): + if ( + isinstance(mcp, StandaloneFastMCP) + and FASTMCP_VERSION is not None + and FASTMCP_VERSION.startswith("2") + ): assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} elif isinstance(mcp, StandaloneFastMCP): assert result.json()["result"]["content"][0]["text"] == json.dumps( From d9c3d378862454a9229a6a44ac5623aaa9254f32 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 12 Jan 2026 16:02:57 +0100 Subject: [PATCH 8/8] make tests work on old fastmcp --- tests/integrations/fastmcp/test_fastmcp.py | 37 ++++++++++++---------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index a9d6306344..35e6bd9d9e 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -404,7 +404,9 @@ async def multiply_numbers(x: int, y: int) -> dict: "result": 42, "operation": "multiplication", } - elif isinstance(mcp, StandaloneFastMCP): + elif ( + isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION is not None + ): # Checking for None is not precise. assert result.json()["result"]["content"][0]["text"] == json.dumps( {"result": 42, "operation": "multiplication"}, ) @@ -824,21 +826,20 @@ async def read_url(resource: str): """Read a URL resource""" return "resource data" - try: - _, result = json_rpc( - app, - method="resources/read", - params={ - "uri": "https://example.com/resource", - }, - ) - except ValueError as e: - # Older FastMCP versions may not support this URI pattern - if "Unknown resource" in str(e): - pytest.skip( - f"Resource URI not supported in this FastMCP version: {e}" - ) - raise + _, result = json_rpc( + app, + method="resources/read", + params={ + "uri": "https://example.com/resource", + }, + ) + # Older FastMCP versions may not support this URI pattern + if ( + "error" in result.json() + and "Unknown resource" in result.json()["error"]["message"] + ): + pytest.skip("Resource URI not supported in this FastMCP version.") + return assert "resource data" in result.json()["result"]["contents"][0]["text"] @@ -1016,7 +1017,9 @@ def http_tool(data: str) -> dict: and FASTMCP_VERSION.startswith("2") ): assert result.json()["result"]["structuredContent"] == {"processed": "TEST"} - elif isinstance(mcp, StandaloneFastMCP): + elif ( + isinstance(mcp, StandaloneFastMCP) and FASTMCP_VERSION is not None + ): # Checking for None is not precise. assert result.json()["result"]["content"][0]["text"] == json.dumps( {"processed": "TEST"}, )