49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
127 lines
4.3 KiB
Python
127 lines
4.3 KiB
Python
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"
|