Spaces:
Running
Running
| import asyncio | |
| import json | |
| import sys | |
| from collections.abc import AsyncGenerator | |
| from unittest.mock import AsyncMock, call | |
| import pytest | |
| import uvicorn | |
| from mcp import McpError | |
| from starlette.applications import Starlette | |
| from starlette.routing import Mount | |
| from fastmcp import Context | |
| from fastmcp.client import Client | |
| from fastmcp.client.transports import StreamableHttpTransport | |
| from fastmcp.server.dependencies import get_http_request | |
| from fastmcp.server.server import FastMCP | |
| from fastmcp.utilities.tests import run_server_in_process | |
| def fastmcp_server(): | |
| """Fixture that creates a FastMCP server with tools, resources, and prompts.""" | |
| server = FastMCP("TestServer") | |
| # Add a tool | |
| def greet(name: str) -> str: | |
| """Greet someone by name.""" | |
| return f"Hello, {name}!" | |
| async def elicit(ctx: Context) -> str: | |
| """Elicit a response from the user.""" | |
| result = await ctx.elicit("What is your name?", response_type=str) | |
| if result.action == "accept": | |
| return f"You said your name was: {result.data}!" | |
| else: | |
| return "No name provided" | |
| # Add a second tool | |
| def add(a: int, b: int) -> int: | |
| """Add two numbers together.""" | |
| return a + b | |
| async def sleep(seconds: float) -> str: | |
| """Sleep for a given number of seconds.""" | |
| await asyncio.sleep(seconds) | |
| return f"Slept for {seconds} seconds" | |
| async def greet_with_progress(name: str, ctx: Context) -> str: | |
| """Report progress for a greeting.""" | |
| await ctx.report_progress(0.5, 1.0, "Greeting in progress") | |
| await ctx.report_progress(0.75, 1.0, "Almost there!") | |
| return f"Hello, {name}!" | |
| # Add a resource | |
| async def get_users(): | |
| return ["Alice", "Bob", "Charlie"] | |
| # Add a resource template | |
| async def get_user(user_id: str): | |
| return {"id": user_id, "name": f"User {user_id}", "active": True} | |
| async def get_headers() -> dict[str, str]: | |
| request = get_http_request() | |
| return dict(request.headers) | |
| # Add a prompt | |
| def welcome(name: str) -> str: | |
| """Example greeting prompt.""" | |
| return f"Welcome to FastMCP, {name}!" | |
| return server | |
| def run_server(host: str, port: int, stateless_http: bool = False, **kwargs) -> None: | |
| server = fastmcp_server() | |
| server.settings.stateless_http = stateless_http | |
| server.run(host=host, port=port, **kwargs) | |
| def run_nested_server(host: str, port: int) -> None: | |
| mcp_app = fastmcp_server().http_app(path="/final/mcp") | |
| mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)]) | |
| mount2 = Starlette( | |
| routes=[Mount("/nest-outer", app=mount)], | |
| lifespan=mcp_app.lifespan, | |
| ) | |
| server = uvicorn.Server( | |
| config=uvicorn.Config( | |
| app=mount2, | |
| host=host, | |
| port=port, | |
| log_level="error", | |
| lifespan="on", | |
| ) | |
| ) | |
| server.run() | |
| async def streamable_http_server( | |
| request, | |
| ) -> AsyncGenerator[str, None]: | |
| stateless_http = getattr(request, "param", False) | |
| with run_server_in_process( | |
| run_server, stateless_http=stateless_http, transport="http" | |
| ) as url: | |
| yield f"{url}/mcp" | |
| async def streamable_http_server_with_streamable_http_alias() -> AsyncGenerator[ | |
| str, None | |
| ]: | |
| """Test that the "streamable-http" transport alias works.""" | |
| with run_server_in_process(run_server, transport="streamable-http") as url: | |
| yield f"{url}/mcp" | |
| async def test_ping(streamable_http_server: str): | |
| """Test pinging the server.""" | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server) | |
| ) as client: | |
| result = await client.ping() | |
| assert result is True | |
| async def test_ping_with_streamable_http_alias( | |
| streamable_http_server_with_streamable_http_alias: str, | |
| ): | |
| """Test pinging the server.""" | |
| async with Client( | |
| transport=StreamableHttpTransport( | |
| streamable_http_server_with_streamable_http_alias | |
| ) | |
| ) as client: | |
| result = await client.ping() | |
| assert result is True | |
| async def test_http_headers(streamable_http_server: str): | |
| """Test getting HTTP headers from the server.""" | |
| async with Client( | |
| transport=StreamableHttpTransport( | |
| streamable_http_server, headers={"X-DEMO-HEADER": "ABC"} | |
| ) | |
| ) as client: | |
| raw_result = await client.read_resource("request://headers") | |
| json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined] | |
| assert "x-demo-header" in json_result | |
| assert json_result["x-demo-header"] == "ABC" | |
| async def test_greet_with_progress_tool(streamable_http_server: str): | |
| """Test calling the greet tool.""" | |
| progress_handler = AsyncMock(return_value=None) | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server), | |
| progress_handler=progress_handler, | |
| ) as client: | |
| result = await client.call_tool("greet_with_progress", {"name": "Alice"}) | |
| assert result.data == "Hello, Alice!" | |
| progress_handler.assert_has_calls( | |
| [ | |
| call(0.5, 1.0, "Greeting in progress"), | |
| call(0.75, 1.0, "Almost there!"), | |
| ] | |
| ) | |
| async def test_elicitation_tool(streamable_http_server: str, request): | |
| """Test calling the elicitation tool in both stateless and stateful modes.""" | |
| async def elicitation_handler(message, response_type, params, ctx): | |
| return {"value": "Alice"} | |
| stateless_http = request.node.callspec.params.get("streamable_http_server", False) | |
| if stateless_http: | |
| pytest.xfail("Elicitation is not supported in stateless HTTP mode") | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server), | |
| elicitation_handler=elicitation_handler, | |
| ) as client: | |
| result = await client.call_tool("elicit") | |
| assert result.data == "You said your name was: Alice!" | |
| async def test_nested_streamable_http_server_resolves_correctly(): | |
| # tests patch for | |
| # https://github.com/modelcontextprotocol/python-sdk/pull/659 | |
| with run_server_in_process(run_nested_server) as url: | |
| async with Client( | |
| transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp") | |
| ) as client: | |
| result = await client.ping() | |
| assert result is True | |
| class TestTimeout: | |
| async def test_timeout(self, streamable_http_server: str): | |
| # note this transport behaves differently than others and raises | |
| # McpError from the *client* context | |
| with pytest.raises(McpError, match="Timed out"): | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server), | |
| timeout=0.1, | |
| ) as client: | |
| await client.call_tool("sleep", {"seconds": 0.2}) | |
| async def test_timeout_tool_call(self, streamable_http_server: str): | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server), | |
| ) as client: | |
| with pytest.raises(McpError): | |
| await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1) | |
| async def test_timeout_tool_call_overrides_client_timeout( | |
| self, streamable_http_server: str | |
| ): | |
| async with Client( | |
| transport=StreamableHttpTransport(streamable_http_server), | |
| timeout=2, | |
| ) as client: | |
| with pytest.raises(McpError): | |
| await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1) | |