"""Tests for InMemoryTransport.""" from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any import anyio import anyio.lowlevel import mcp_types as types import pytest from mcp_types import ListResourcesResult, Resource from mcp import Client from mcp.client import _memory from mcp.client._memory import InMemoryTransport from mcp.server import Server, ServerRequestContext from mcp.server.mcpserver import MCPServer @pytest.fixture def simple_server() -> Server: """Create a simple MCP server for testing.""" async def handle_list_resources( ctx: ServerRequestContext, params: types.PaginatedRequestParams | None ) -> ListResourcesResult: # pragma: no cover return ListResourcesResult( resources=[ Resource( uri="memory://test", name="Test Resource", description="A test resource", ) ] ) return Server(name="test_server", on_list_resources=handle_list_resources) @pytest.fixture def mcpserver_server() -> MCPServer: """Create an MCPServer server for testing.""" server = MCPServer("test") @server.tool() def greet(name: str) -> str: """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" return server pytestmark = pytest.mark.anyio async def test_with_server(simple_server: Server): """Test creating transport with a Server instance.""" transport = InMemoryTransport(simple_server) async with transport as (read_stream, write_stream): assert read_stream is not None assert write_stream is not None async def test_with_mcpserver(mcpserver_server: MCPServer): """Test creating transport with an MCPServer instance.""" transport = InMemoryTransport(mcpserver_server) async with transport as (read_stream, write_stream): assert read_stream is not None assert write_stream is not None async def test_server_is_running(mcpserver_server: MCPServer): """Test that the server is running and responding to requests.""" async with Client(mcpserver_server, mode="legacy") as client: assert client.server_capabilities.tools is not None async def test_list_tools(mcpserver_server: MCPServer): """Test listing tools through the transport.""" async with Client(mcpserver_server, mode="legacy") as client: tools_result = await client.list_tools() assert len(tools_result.tools) > 0 tool_names = [t.name for t in tools_result.tools] assert "greet" in tool_names async def test_call_tool(mcpserver_server: MCPServer): """Test calling a tool through the transport.""" async with Client(mcpserver_server, mode="legacy") as client: result = await client.call_tool("greet", {"name": "World"}) assert result is not None assert len(result.content) > 0 assert "Hello, World!" in str(result.content[0]) async def test_raise_exceptions(mcpserver_server: MCPServer): """Test that raise_exceptions parameter is passed through.""" transport = InMemoryTransport(mcpserver_server, raise_exceptions=True) async with transport as (read_stream, _write_stream): assert read_stream is not None async def test_aexit_with_well_behaved_lifespan_runs_teardown_without_cancel(): """A lifespan that finishes promptly on EOF should run to completion. The transport closes the streams first and waits for the server to exit naturally, so teardown observes no cancellation. """ teardown_ran = anyio.Event() @asynccontextmanager async def lifespan(_: Server[Any]) -> AsyncIterator[dict[str, Any]]: yield {} await anyio.lowlevel.checkpoint() teardown_ran.set() server = Server(name="test_server", lifespan=lifespan) with anyio.fail_after(5): async with InMemoryTransport(server): pass assert teardown_ran.is_set() async def test_aexit_with_blocking_lifespan_is_bounded(monkeypatch: pytest.MonkeyPatch): """A lifespan that never returns must not hang `__aexit__` forever. After EOFing the server the transport waits `SERVER_SHUTDOWN_GRACE` for a natural exit, then cancels the server task as a backstop so the task-group join completes. """ monkeypatch.setattr(_memory, "SERVER_SHUTDOWN_GRACE", 0.05) teardown_started = anyio.Event() @asynccontextmanager async def blocking_lifespan(_: Server[Any]) -> AsyncIterator[dict[str, Any]]: yield {} teardown_started.set() await anyio.Event().wait() server = Server(name="test_server", lifespan=blocking_lifespan) with anyio.fail_after(5): async with InMemoryTransport(server): pass assert teardown_started.is_set()