Add ergonomic Client class for testing MCP servers (#1870)

This commit is contained in:
Felix Weinberger 2026-01-16 15:49:26 +00:00 • committed by GitHub
commit df039bf97c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1298 additions and 526 deletions

View file

@ -123,11 +123,13 @@ def stream_spy() -> Generator[Callable[[], StreamSpyCollection], None, None]:
yield (client_read, spy_client_write), (server_read, spy_server_write)
# Apply the patch for the duration of the test
# Patch both locations since InMemoryTransport imports it directly
with patch("mcp.shared.memory.create_client_server_memory_streams", patched_create_streams):
# Return a collection with helper methods
def get_spy_collection() -> StreamSpyCollection:
assert client_spy is not None, "client_spy was not initialized"
assert server_spy is not None, "server_spy was not initialized"
return StreamSpyCollection(client_spy, server_spy)
with patch("mcp.client._memory.create_client_server_memory_streams", patched_create_streams):
# Return a collection with helper methods
def get_spy_collection() -> StreamSpyCollection:
assert client_spy is not None, "client_spy was not initialized"
assert server_spy is not None, "server_spy was not initialized"
return StreamSpyCollection(client_spy, server_spy)
yield get_spy_collection
yield get_spy_collection

336
tests/client/test_client.py Normal file
View file

@ -0,0 +1,336 @@
"""Tests for the unified Client class."""
from unittest.mock import AsyncMock, patch
import pytest
import mcp.types as types
from mcp.client.client import Client
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.types import EmptyResult, Resource
pytestmark = pytest.mark.anyio
@pytest.fixture
def simple_server() -> Server:
"""Create a simple MCP server for testing."""
server = Server(name="test_server")
@server.list_resources()
async def handle_list_resources():
return [
Resource(
uri="memory://test",
name="Test Resource",
description="A test resource",
)
]
return server
@pytest.fixture
def app() -> FastMCP:
"""Create a FastMCP server for testing."""
server = FastMCP("test")
@server.tool()
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@server.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@server.resource("test://resource")
def test_resource() -> str:
"""A test resource."""
return "Test content"
@server.prompt()
def greeting_prompt(name: str) -> str:
"""A greeting prompt."""
return f"Please greet {name} warmly."
return server
async def test_creates_client(app: FastMCP):
"""Test that from_server creates a connected client."""
async with Client(app) as client:
assert client is not None
async def test_client_is_initialized(app: FastMCP):
"""Test that the client is initialized after entering context."""
async with Client(app) as client:
caps = client.server_capabilities
assert caps is not None
assert caps.tools is not None
async def test_with_simple_server(simple_server: Server):
"""Test that from_server works with a basic Server instance."""
async with Client(simple_server) as client:
assert client is not None
caps = client.server_capabilities
assert caps is not None
# Verify list_resources works and returns expected resource
resources = await client.list_resources()
assert len(resources.resources) == 1
assert resources.resources[0].uri == "memory://test"
async def test_ping_returns_empty_result(app: FastMCP):
"""Test that ping returns an EmptyResult."""
async with Client(app) as client:
result = await client.send_ping()
assert isinstance(result, EmptyResult)
async def test_list_tools(app: FastMCP):
"""Test listing tools."""
async with Client(app) as client:
result = await client.list_tools()
assert result.tools is not None
tool_names = [t.name for t in result.tools]
assert "greet" in tool_names
assert "add" in tool_names
async def test_list_tools_with_pagination(app: FastMCP):
"""Test listing tools with pagination params."""
from mcp.types import PaginatedRequestParams
async with Client(app) as client:
result = await client.list_tools(params=PaginatedRequestParams())
assert result.tools is not None
async def test_call_tool(app: FastMCP):
"""Test calling a tool."""
async with Client(app) as client:
result = await client.call_tool("greet", {"name": "World"})
assert result.content is not None
assert len(result.content) > 0
content_str = str(result.content[0])
assert "Hello, World!" in content_str
async def test_call_tool_with_multiple_args(app: FastMCP):
"""Test calling a tool with multiple arguments."""
async with Client(app) as client:
result = await client.call_tool("add", {"a": 5, "b": 3})
assert result.content is not None
content_str = str(result.content[0])
assert "8" in content_str
async def test_list_resources(app: FastMCP):
"""Test listing resources."""
async with Client(app) as client:
result = await client.list_resources()
# FastMCP may have different resource listing behavior
assert result is not None
async def test_read_resource(app: FastMCP):
"""Test reading a resource."""
async with Client(app) as client:
result = await client.read_resource("test://resource")
assert result.contents is not None
assert len(result.contents) > 0
async def test_list_prompts(app: FastMCP):
"""Test listing prompts."""
async with Client(app) as client:
result = await client.list_prompts()
prompt_names = [p.name for p in result.prompts]
assert "greeting_prompt" in prompt_names
async def test_get_prompt(app: FastMCP):
"""Test getting a prompt."""
async with Client(app) as client:
result = await client.get_prompt("greeting_prompt", {"name": "Alice"})
assert result.messages is not None
assert len(result.messages) > 0
async def test_session_property(app: FastMCP):
"""Test that the session property returns the ClientSession."""
from mcp.client.session import ClientSession
async with Client(app) as client:
session = client.session
assert isinstance(session, ClientSession)
async def test_session_is_same_as_internal(app: FastMCP):
"""Test that session property returns consistent instance."""
async with Client(app) as client:
session1 = client.session
session2 = client.session
assert session1 is session2
async def test_enters_and_exits_cleanly(app: FastMCP):
"""Test that the client enters and exits cleanly."""
async with Client(app) as client:
# Should be able to use client
await client.send_ping()
# After exiting, resources should be cleaned up
async def test_exception_during_use(app: FastMCP):
"""Test that exceptions during use don't prevent cleanup."""
with pytest.raises(Exception): # May be wrapped in ExceptionGroup by anyio
async with Client(app) as client:
await client.send_ping()
raise ValueError("Test exception")
# Should exit cleanly despite exception
async def test_aexit_without_aenter(app: FastMCP):
"""Test that calling __aexit__ without __aenter__ doesn't raise."""
client = Client(app)
# This should not raise even though __aenter__ was never called
await client.__aexit__(None, None, None)
assert client._session is None
async def test_server_capabilities_after_init(app: FastMCP):
"""Test server_capabilities property after initialization."""
async with Client(app) as client:
caps = client.server_capabilities
assert caps is not None
# FastMCP should advertise tools capability
assert caps.tools is not None
def test_session_property_before_enter(app: FastMCP):
"""Test that accessing session before context manager raises RuntimeError."""
client = Client(app)
with pytest.raises(RuntimeError, match="Client must be used within an async context manager"):
_ = client.session
async def test_reentry_raises_runtime_error(app: FastMCP):
"""Test that reentering a client raises RuntimeError."""
async with Client(app) as client:
with pytest.raises(RuntimeError, match="Client is already entered"):
await client.__aenter__()
async def test_cleanup_on_init_failure(app: FastMCP):
"""Test that resources are cleaned up if initialization fails."""
with patch("mcp.client.client.ClientSession") as mock_session_class:
# Create a mock context manager that fails on __aenter__
mock_session = AsyncMock()
mock_session.__aenter__.side_effect = RuntimeError("Session init failed")
mock_session.__aexit__ = AsyncMock(return_value=None)
mock_session_class.return_value = mock_session
client = Client(app)
with pytest.raises(BaseException) as exc_info:
await client.__aenter__()
# The error should contain our message (may be wrapped in ExceptionGroup)
# Use repr() to see nested exceptions in ExceptionGroup
assert "Session init failed" in repr(exc_info.value)
# Verify the client is in a clean state (session should be None)
assert client._session is None
async def test_send_progress_notification(app: FastMCP):
"""Test sending progress notification."""
async with Client(app) as client:
# Send a progress notification - this should not raise
await client.send_progress_notification(
progress_token="test-token",
progress=50.0,
total=100.0,
message="Half done",
)
async def test_subscribe_resource(app: FastMCP):
"""Test subscribing to a resource."""
async with Client(app) as client:
# Mock the session's subscribe_resource since FastMCP doesn't support it
with patch.object(client.session, "subscribe_resource", return_value=EmptyResult()):
result = await client.subscribe_resource("test://resource")
assert isinstance(result, EmptyResult)
async def test_unsubscribe_resource(app: FastMCP):
"""Test unsubscribing from a resource."""
async with Client(app) as client:
# Mock the session's unsubscribe_resource since FastMCP doesn't support it
with patch.object(client.session, "unsubscribe_resource", return_value=EmptyResult()):
result = await client.unsubscribe_resource("test://resource")
assert isinstance(result, EmptyResult)
async def test_send_roots_list_changed(app: FastMCP):
"""Test sending roots list changed notification."""
async with Client(app) as client:
# Send roots list changed notification - should not raise
await client.send_roots_list_changed()
async def test_set_logging_level(app: FastMCP):
"""Test setting logging level."""
async with Client(app) as client:
# Mock the session's set_logging_level since FastMCP doesn't support it
with patch.object(client.session, "set_logging_level", return_value=EmptyResult()):
result = await client.set_logging_level("debug")
assert isinstance(result, EmptyResult)
async def test_list_resources_with_params(app: FastMCP):
"""Test listing resources with params parameter."""
async with Client(app) as client:
result = await client.list_resources(params=types.PaginatedRequestParams())
assert result is not None
async def test_list_resource_templates_with_params(app: FastMCP):
"""Test listing resource templates with params parameter."""
async with Client(app) as client:
result = await client.list_resource_templates(params=types.PaginatedRequestParams())
assert result is not None
async def test_list_resource_templates_default(app: FastMCP):
"""Test listing resource templates with no params or cursor."""
async with Client(app) as client:
result = await client.list_resource_templates()
assert result is not None
async def test_list_prompts_with_params(app: FastMCP):
"""Test listing prompts with params parameter."""
async with Client(app) as client:
result = await client.list_prompts(params=types.PaginatedRequestParams())
assert result is not None
async def test_complete_with_prompt_reference(app: FastMCP):
"""Test getting completions for a prompt argument."""
async with Client(app) as client:
ref = types.PromptReference(type="ref/prompt", name="greeting_prompt")
# Mock the session's complete method since FastMCP may not support it
with patch.object(
client.session,
"complete",
return_value=types.CompleteResult(completion=types.Completion(values=[])),
):
result = await client.complete(ref=ref, argument={"name": "test"})
assert result is not None

View file

@ -3,9 +3,10 @@ from collections.abc import Callable
import pytest
import mcp.types as types
from mcp.client._memory import InMemoryTransport
from mcp.client.session import ClientSession
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
@ -18,30 +19,28 @@ 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"""
# 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("resource://test/{name}")
async def test_template(name: str) -> str: # pragma: no cover
"""Test resource template"""
return f"Data for {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
@ -61,78 +60,82 @@ async def test_list_methods_params_parameter(
method_name: str,
request_method: str,
):
"""Test that the params parameter works correctly for list methods.
"""Test that the params parameter is accepted and correctly passed to the server.
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.
See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format
"""
async with create_session(full_featured_server._mcp_server) as client_session:
spies = stream_spy()
method = getattr(client_session, method_name)
transport = InMemoryTransport(full_featured_server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
spies = stream_spy()
# 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
# Test without params (omitted)
method = getattr(session, method_name)
_ = await method()
requests = spies.get_client_requests(method=request_method)
assert len(requests) == 1
assert requests[0].params is None
spies.clear()
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
# Test with params containing cursor
_ = await method(params=types.PaginatedRequestParams(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()
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"
# Test with empty params
_ = await method(params=types.PaginatedRequestParams())
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():
"""Test that list_tools works with strict servers require a params field,
even if it is empty.
async def test_list_tools_with_strict_server_validation(
full_featured_server: FastMCP,
):
"""Test pagination with a server that validates request format strictly."""
transport = InMemoryTransport(full_featured_server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
result = await session.list_tools(params=types.PaginatedRequestParams())
assert isinstance(result, ListToolsResult)
assert len(result.tools) > 0
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")
async def test_list_tools_with_lowlevel_server():
"""Test that list_tools works with a lowlevel Server using params."""
server = Server("test-lowlevel")
@server.list_tools()
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult: # pragma: no cover
"""Strict handler that validates params field exists"""
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
# Echo back what cursor we received in the tool description
cursor = request.params.cursor if request.params else None
return ListToolsResult(
tools=[
types.Tool(
name="test_tool",
description=f"cursor={cursor}",
input_schema={},
)
]
)
# 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."
)
transport = InMemoryTransport(server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# Return empty tools list
return ListToolsResult(tools=[])
result = await session.list_tools(params=types.PaginatedRequestParams())
assert result.tools[0].description == "cursor=None"
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
result = await session.list_tools(params=types.PaginatedRequestParams(cursor="page2"))
assert result.tools[0].description == "cursor=page2"

View file

@ -1,14 +1,12 @@
import pytest
from pydantic import FileUrl
from mcp import Client
from mcp.client.session import ClientSession
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.server import Context
from mcp.server.session import ServerSession
from mcp.shared.context import RequestContext
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
from mcp.types import ListRootsResult, Root, TextContent
@ -41,17 +39,17 @@ async def test_list_roots_callback():
return True
# Test with list_roots callback
async with create_session(server._mcp_server, list_roots_callback=list_roots_callback) as client_session:
async with Client(server, list_roots_callback=list_roots_callback) as client:
# Make a request to trigger sampling callback
result = await client_session.call_tool("test_list_roots", {"message": "test message"})
result = await client.call_tool("test_list_roots", {"message": "test message"})
assert result.is_error is False
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "true"
# Test without list_roots callback
async with create_session(server._mcp_server) as client_session:
async with Client(server) as client:
# Make a request to trigger sampling callback
result = await client_session.call_tool("test_list_roots", {"message": "test message"})
result = await client.call_tool("test_list_roots", {"message": "test message"})
assert result.is_error is True
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Error executing tool test_list_roots: List roots not supported"

View file

@ -3,10 +3,8 @@ from typing import Any, Literal
import pytest
import mcp.types as types
from mcp import Client
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
from mcp.shared.session import RequestResponder
from mcp.types import (
LoggingMessageNotificationParams,
@ -70,19 +68,19 @@ async def test_logging_callback():
if isinstance(message, Exception): # pragma: no cover
raise message
async with create_session(
server._mcp_server,
async with Client(
server,
logging_callback=logging_collector,
message_handler=message_handler,
) as client_session:
) as client:
# First verify our test tool works
result = await client_session.call_tool("test_tool", {})
result = await client.call_tool("test_tool", {})
assert result.is_error is False
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "true"
# Now send a log message via our tool
log_result = await client_session.call_tool(
log_result = await client.call_tool(
"test_tool_with_log",
{
"message": "Test log message",
@ -90,7 +88,7 @@ async def test_logging_callback():
"logger": "test_logger",
},
)
log_result_with_extra = await client_session.call_tool(
log_result_with_extra = await client.call_tool(
"test_tool_with_log_extra",
{
"message": "Test log message",

View file

@ -7,10 +7,8 @@ from unittest.mock import patch
import jsonschema
import pytest
from mcp import Client
from mcp.server.lowlevel import Server
from mcp.shared.memory import (
create_connected_server_and_client_session as client_session,
)
from mcp.types import Tool
@ -77,7 +75,7 @@ class TestClientOutputSchemaValidation:
# Test that client validates the structured content
with bypass_server_output_validation():
async with client_session(server) as client:
async with Client(server) as client:
# The client validates structured content and should raise an error
with pytest.raises(RuntimeError) as exc_info:
await client.call_tool("get_user", {})
@ -114,7 +112,7 @@ class TestClientOutputSchemaValidation:
return {"result": "not_a_number"} # Invalid: should be int
with bypass_server_output_validation():
async with client_session(server) as client:
async with Client(server) as client:
# The client validates structured content and should raise an error
with pytest.raises(RuntimeError) as exc_info:
await client.call_tool("calculate", {})
@ -145,7 +143,7 @@ class TestClientOutputSchemaValidation:
return {"alice": "100", "bob": "85"} # Invalid: values should be int
with bypass_server_output_validation():
async with client_session(server) as client:
async with Client(server) as client:
# The client validates structured content and should raise an error
with pytest.raises(RuntimeError) as exc_info:
await client.call_tool("get_scores", {})
@ -180,7 +178,7 @@ class TestClientOutputSchemaValidation:
return {"name": "John", "age": 30} # Missing required 'email'
with bypass_server_output_validation():
async with client_session(server) as client:
async with Client(server) as client:
# The client validates structured content and should raise an error
with pytest.raises(RuntimeError) as exc_info:
await client.call_tool("get_person", {})
@ -205,7 +203,7 @@ class TestClientOutputSchemaValidation:
caplog.set_level(logging.WARNING)
with bypass_server_output_validation():
async with client_session(server) as client:
async with Client(server) as client:
# Call a tool that wasn't listed
result = await client.call_tool("mystery_tool", {})
assert result.structured_content == {"result": 42}

View file

@ -1,11 +1,9 @@
import pytest
from mcp import Client
from mcp.client.session import ClientSession
from mcp.server.fastmcp import FastMCP
from mcp.shared.context import RequestContext
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
from mcp.types import (
CreateMessageRequestParams,
CreateMessageResult,
@ -43,17 +41,17 @@ async def test_sampling_callback():
return True
# Test with sampling callback
async with create_session(server._mcp_server, sampling_callback=sampling_callback) as client_session:
async with Client(server, sampling_callback=sampling_callback) as client:
# Make a request to trigger sampling callback
result = await client_session.call_tool("test_sampling", {"message": "Test message for sampling"})
result = await client.call_tool("test_sampling", {"message": "Test message for sampling"})
assert result.is_error is False
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "true"
# Test without sampling callback
async with create_session(server._mcp_server) as client_session:
async with Client(server) as client:
# Make a request to trigger sampling callback
result = await client_session.call_tool("test_sampling", {"message": "Test message for sampling"})
result = await client.call_tool("test_sampling", {"message": "Test message for sampling"})
assert result.is_error is True
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Error executing tool test_sampling: Sampling not supported"
@ -94,8 +92,8 @@ async def test_create_message_backwards_compat_single_content():
assert not hasattr(result, "content_as_list") or not callable(getattr(result, "content_as_list", None))
return True
async with create_session(server._mcp_server, sampling_callback=sampling_callback) as client_session:
result = await client_session.call_tool("test_backwards_compat", {"message": "Test"})
async with Client(server, sampling_callback=sampling_callback) as client:
result = await client.call_tool("test_backwards_compat", {"message": "Test"})
assert result.is_error is False
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "true"

View file

View file

@ -0,0 +1,114 @@
"""Tests for InMemoryTransport."""
import pytest
from mcp.client._memory import InMemoryTransport
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.types import Resource
@pytest.fixture
def simple_server() -> Server:
"""Create a simple MCP server for testing."""
server = Server(name="test_server")
# pragma: no cover - handler exists only to register a resource capability.
# Transport tests verify stream creation, not handler invocation.
@server.list_resources()
async def handle_list_resources(): # pragma: no cover
return [
Resource(
uri="memory://test",
name="Test Resource",
description="A test resource",
)
]
return server
@pytest.fixture
def fastmcp_server() -> FastMCP:
"""Create a FastMCP server for testing."""
server = FastMCP("test")
# pragma: no cover on handlers below - they exist only to register capabilities.
# Transport tests verify stream creation and basic protocol, not handler 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"
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.connect() as (read_stream, write_stream):
assert read_stream is not None
assert write_stream is not None
async def test_with_fastmcp(fastmcp_server: FastMCP):
"""Test creating transport with a FastMCP instance."""
transport = InMemoryTransport(fastmcp_server)
async with transport.connect() as (read_stream, write_stream):
assert read_stream is not None
assert write_stream is not None
async def test_server_is_running(fastmcp_server: FastMCP):
"""Test that the server is running and responding to requests."""
from mcp.client.session import ClientSession
transport = InMemoryTransport(fastmcp_server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
result = await session.initialize()
assert result is not None
assert result.server_info.name == "test"
async def test_list_tools(fastmcp_server: FastMCP):
"""Test listing tools through the transport."""
from mcp.client.session import ClientSession
transport = InMemoryTransport(fastmcp_server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools_result = await session.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(fastmcp_server: FastMCP):
"""Test calling a tool through the transport."""
from mcp.client.session import ClientSession
transport = InMemoryTransport(fastmcp_server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
result = await session.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(fastmcp_server: FastMCP):
"""Test that raise_exceptions parameter is passed through."""
transport = InMemoryTransport(fastmcp_server, raise_exceptions=True)
async with transport.connect() as (read_stream, _write_stream):
assert read_stream is not None