Files
modelcontextprotocol--pytho…/tests/client/transports/test_memory.py
T
wehub-resource-sync 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) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

150 lines
4.9 KiB
Python

"""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()