from collections.abc import Callable import mcp_types as types import pytest from mcp_types import ListToolsResult from mcp import Client from mcp.server import Server, ServerRequestContext from mcp.server.mcpserver import MCPServer from .conftest import StreamSpyCollection pytestmark = pytest.mark.anyio @pytest.fixture async def full_featured_server(): """Create a server with tools, resources, prompts, and templates.""" server = MCPServer("test") # pragma: no cover on handlers below - these exist only to register items with the # server so list_* methods return results. The handlers themselves are never called # because these tests only verify pagination/cursor behavior, not tool/resource invocation. @server.tool() def greet(name: str) -> str: # pragma: no cover """Greet someone by name.""" return f"Hello, {name}!" @server.resource("test://resource") def test_resource() -> str: # pragma: no cover """A test resource.""" return "Test content" @server.resource("test://template/{id}") def test_template(id: str) -> str: # pragma: no cover """A test resource template.""" return f"Template content for {id}" @server.prompt() def greeting_prompt(name: str) -> str: # pragma: no cover """A greeting prompt.""" return f"Please greet {name}." return server @pytest.mark.parametrize( "method_name,request_method", [ ("list_tools", "tools/list"), ("list_resources", "resources/list"), ("list_prompts", "prompts/list"), ("list_resource_templates", "resources/templates/list"), ], ) async def test_list_methods_params_parameter( stream_spy: Callable[[], StreamSpyCollection], full_featured_server: MCPServer, method_name: str, request_method: str, ): """Test that the params parameter is accepted and correctly passed to the server. Covers: list_tools, list_resources, list_prompts, list_resource_templates See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format """ async with Client(full_featured_server, mode="legacy") as client: spies = stream_spy() # Test without params (omitted) method = getattr(client, method_name) _ = await method() requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is None or "cursor" not in requests[0].params spies.clear() # Test with params containing cursor _ = await method(cursor="from_params") requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is not None assert requests[0].params["cursor"] == "from_params" spies.clear() # Test with empty params _ = await method() requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 # Empty params means no cursor assert requests[0].params is None or "cursor" not in requests[0].params async def test_list_tools_with_strict_server_validation( full_featured_server: MCPServer, ): """Test pagination with a server that validates request format strictly.""" async with Client(full_featured_server) as client: result = await client.list_tools() assert isinstance(result, ListToolsResult) assert len(result.tools) > 0 async def test_list_tools_with_lowlevel_server(): """Test that list_tools works with a lowlevel Server using params.""" async def handle_list_tools( ctx: ServerRequestContext, params: types.PaginatedRequestParams | None ) -> ListToolsResult: # Echo back what cursor we received in the tool description cursor = params.cursor if params else None return ListToolsResult( tools=[types.Tool(name="test_tool", description=f"cursor={cursor}", input_schema={"type": "object"})] ) server = Server("test-lowlevel", on_list_tools=handle_list_tools) async with Client(server) as client: result = await client.list_tools() assert result.tools[0].description == "cursor=None" result = await client.list_tools(cursor="page2") assert result.tools[0].description == "cursor=page2"