from collections.abc import Callable import pytest import mcp.types as types from mcp.server import Server from mcp.server.fastmcp import FastMCP from mcp.shared.memory import create_connected_server_and_client_session as create_session from mcp.types import ListToolsRequest, ListToolsResult 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 = FastMCP("test") @server.tool(name="test_tool_1") async def test_tool_1() -> str: # pragma: no cover """First test tool""" return "Result 1" @server.tool(name="test_tool_2") async def test_tool_2() -> str: # pragma: no cover """Second test tool""" return "Result 2" @server.resource("resource://test/data") async def test_resource() -> str: # pragma: no cover """Test resource""" return "Test data" @server.prompt() async def test_prompt(name: str) -> str: # pragma: no cover """Test prompt""" return f"Hello, {name}!" @server.resource("resource://test/{name}") async def test_template(name: str) -> str: # pragma: no cover """Test resource template""" return f"Data for {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: FastMCP, method_name: str, request_method: str, ): """Test that the params parameter works correctly for list methods. Covers: list_tools, list_resources, list_prompts, list_resource_templates This tests the new params parameter API (non-deprecated) to ensure it correctly handles all parameter combinations. """ async with create_session(full_featured_server._mcp_server) as client_session: spies = stream_spy() method = getattr(client_session, method_name) # Test without params parameter (omitted) _ = await method() requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is None spies.clear() # Test with params=None _ = await method(params=None) requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is None spies.clear() # Test with empty params (for strict servers) _ = await method(params=types.PaginatedRequestParams()) requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is not None assert requests[0].params.get("cursor") is None spies.clear() # Test with params containing cursor _ = await method(params=types.PaginatedRequestParams(cursor="some_cursor_value")) requests = spies.get_client_requests(method=request_method) assert len(requests) == 1 assert requests[0].params is not None assert requests[0].params["cursor"] == "some_cursor_value" async def test_list_tools_with_strict_server_validation(): """Test that list_tools works with strict servers require a params field, even if it is empty. Some MCP servers may implement strict JSON-RPC validation that requires the params field to always be present in requests, even if empty {}. This test ensures such servers are supported by the client SDK for list_resources requests without a cursor. """ server = Server("strict_server") @server.list_tools() async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult: # pragma: no cover """Strict handler that validates params field exists""" # Simulate strict server validation if request.params is None: raise ValueError( "Strict server validation failed: params field must be present. " "Expected params: {} for requests without cursor." ) # Return empty tools list return ListToolsResult(tools=[]) async with create_session(server) as client_session: # Use params to explicitly send params: {} for strict server compatibility result = await client_session.list_tools(params=types.PaginatedRequestParams()) assert result is not None