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
7 changed files with 1298 additions and 526 deletions

View file

@ -7,12 +7,11 @@ from typing import Any
import pytest
from pydantic import BaseModel, Field
from mcp import types
from mcp import Client, types
from mcp.client.session import ClientSession, ElicitationFnT
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.shared.context import RequestContext
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import ElicitRequestParams, ElicitResult, TextContent
@ -47,12 +46,8 @@ async def call_tool_and_assert(
text_contains: list[str] | None = None,
):
"""Helper to create session, call tool, and assert result."""
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool(tool_name, args)
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool(tool_name, args)
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
@ -134,14 +129,10 @@ async def test_elicitation_schema_validation():
): # pragma: no cover
return ElicitResult(action="accept", content={})
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
# Test both invalid schemas
for tool_name, field_name in [("invalid_list", "numbers"), ("nested_model", "nested")]:
result = await client_session.call_tool(tool_name, {})
result = await client.call_tool(tool_name, {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert "Validation failed as expected" in result.content[0].text

View file

@ -8,6 +8,7 @@ from pydantic import BaseModel
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from mcp.client import Client
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.prompts.base import Message, UserMessage
@ -16,7 +17,6 @@ from mcp.server.fastmcp.utilities.types import Audio, Image
from mcp.server.session import ServerSession
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.exceptions import McpError
from mcp.shared.memory import create_connected_server_and_client_session as client_session
from mcp.types import (
AudioContent,
BlobResourceContents,
@ -77,7 +77,7 @@ class TestServer:
def hello_world(name: str = "世界") -> str:
return f"¡Hola, {name}! 👋"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools.tools) == 1
tool = tools.tools[0]
@ -230,7 +230,7 @@ class TestServerTools:
async def test_list_tools(self):
mcp = FastMCP()
mcp.add_tool(tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools.tools) == 1
@ -238,7 +238,7 @@ class TestServerTools:
async def test_call_tool(self):
mcp = FastMCP()
mcp.add_tool(tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("my_tool", {"arg1": "value"})
assert not hasattr(result, "error")
assert len(result.content) > 0
@ -247,7 +247,7 @@ class TestServerTools:
async def test_tool_exception_handling(self):
mcp = FastMCP()
mcp.add_tool(error_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("error_tool_fn", {})
assert len(result.content) == 1
content = result.content[0]
@ -259,7 +259,7 @@ class TestServerTools:
async def test_tool_error_handling(self):
mcp = FastMCP()
mcp.add_tool(error_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("error_tool_fn", {})
assert len(result.content) == 1
content = result.content[0]
@ -272,7 +272,7 @@ class TestServerTools:
"""Test that exception details are properly formatted in the response"""
mcp = FastMCP()
mcp.add_tool(error_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("error_tool_fn", {})
content = result.content[0]
assert isinstance(content, TextContent)
@ -284,7 +284,7 @@ class TestServerTools:
async def test_tool_return_value_conversion(self):
mcp = FastMCP()
mcp.add_tool(tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("tool_fn", {"x": 1, "y": 2})
assert len(result.content) == 1
content = result.content[0]
@ -302,7 +302,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(image_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("image_tool_fn", {"path": str(image_path)})
assert len(result.content) == 1
content = result.content[0]
@ -323,7 +323,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(audio_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("audio_tool_fn", {"path": str(audio_path)})
assert len(result.content) == 1
content = result.content[0]
@ -358,7 +358,7 @@ class TestServerTools:
audio_path = tmp_path / filename
audio_path.write_bytes(b"fake audio data")
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("audio_tool_fn", {"path": str(audio_path)})
assert len(result.content) == 1
content = result.content[0]
@ -373,7 +373,7 @@ class TestServerTools:
async def test_tool_mixed_content(self):
mcp = FastMCP()
mcp.add_tool(mixed_content_tool_fn)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("mixed_content_tool_fn", {})
assert len(result.content) == 3
content1, content2, content3 = result.content
@ -425,7 +425,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(mixed_list_fn) # type: ignore
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("mixed_list_fn", {})
assert len(result.content) == 5
# Check text conversion
@ -469,7 +469,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(get_user)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
# Check that the tool has outputSchema
tools = await client.list_tools()
tool = next(t for t in tools.tools if t.name == "get_user")
@ -499,7 +499,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(calculate_sum)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
# Check that the tool has outputSchema
tools = await client.list_tools()
tool = next(t for t in tools.tools if t.name == "calculate_sum")
@ -526,7 +526,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(get_numbers)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("get_numbers", {})
assert result.is_error is False
assert result.structured_content is not None
@ -542,7 +542,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(get_numbers)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("get_numbers", {})
assert result.is_error is True
assert result.structured_content is None
@ -566,7 +566,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(get_metadata)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
# Check schema
tools = await client.list_tools()
tool = next(t for t in tools.tools if t.name == "get_metadata")
@ -602,7 +602,7 @@ class TestServerTools:
mcp = FastMCP()
mcp.add_tool(get_settings)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
# Check schema
tools = await client.list_tools()
tool = next(t for t in tools.tools if t.name == "get_settings")
@ -646,7 +646,7 @@ class TestServerTools:
mcp.add_tool(error_tool_fn)
# Verify both tools exist
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools.tools) == 2
tool_names = [t.name for t in tools.tools]
@ -657,7 +657,7 @@ class TestServerTools:
mcp.remove_tool("tool_fn")
# Verify only one tool remains
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools.tools) == 1
assert tools.tools[0].name == "error_tool_fn"
@ -669,7 +669,7 @@ class TestServerTools:
mcp.add_tool(tool_fn)
# Verify tool works before removal
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("tool_fn", {"x": 1, "y": 2})
assert not result.is_error
content = result.content[0]
@ -680,7 +680,7 @@ class TestServerTools:
mcp.remove_tool("tool_fn")
# Verify calling removed tool returns an error
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("tool_fn", {"x": 1, "y": 2})
assert result.is_error
content = result.content[0]
@ -699,8 +699,12 @@ class TestServerResources:
resource = FunctionResource(uri="resource://test", name="test", fn=get_text)
mcp.add_resource(resource)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://test")
async with Client(mcp) as client:
result = await client.read_resource("resource://test")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Hello, world!"
@ -719,8 +723,12 @@ class TestServerResources:
)
mcp.add_resource(resource)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://binary")
async with Client(mcp) as client:
result = await client.read_resource("resource://binary")
assert isinstance(result.contents[0], BlobResourceContents)
assert result.contents[0].blob == base64.b64encode(b"Binary data").decode()
@ -735,8 +743,12 @@ class TestServerResources:
resource = FileResource(uri="file://test.txt", name="test.txt", path=text_file)
mcp.add_resource(resource)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("file://test.txt")
async with Client(mcp) as client:
result = await client.read_resource("file://test.txt")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Hello from file!"
@ -756,8 +768,12 @@ class TestServerResources:
)
mcp.add_resource(resource)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("file://test.bin")
async with Client(mcp) as client:
result = await client.read_resource("file://test.bin")
assert isinstance(result.contents[0], BlobResourceContents)
assert result.contents[0].blob == base64.b64encode(b"Binary file data").decode()
@ -770,7 +786,7 @@ class TestServerResources:
"""get_data returns a string"""
return "Hello, world!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
resources = await client.list_resources()
assert len(resources.resources) == 1
resource = resources.resources[0]
@ -822,8 +838,12 @@ class TestServerResourceTemplates:
def get_data(name: str) -> str:
return f"Data for {name}"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Data for test"
@ -847,8 +867,12 @@ class TestServerResourceTemplates:
def get_data(org: str, repo: str) -> str:
return f"Data for {org}/{repo}"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://cursor/fastmcp/data")
async with Client(mcp) as client:
result = await client.read_resource("resource://cursor/fastmcp/data")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Data for cursor/fastmcp"
@ -870,8 +894,12 @@ class TestServerResourceTemplates:
def get_static_data() -> str:
return "Static data"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://static")
async with Client(mcp) as client:
result = await client.read_resource("resource://static")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Static data"
@ -910,8 +938,12 @@ class TestServerResourceTemplates:
assert hasattr(template, "mime_type")
assert template.mime_type == "text/csv"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://bob/csv")
async with Client(mcp) as client:
result = await client.read_resource("resource://bob/csv")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "csv for bob"
@ -972,7 +1004,10 @@ class TestServerResourceMetadata:
def get_data() -> str:
return "test data"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
# Verify content and metadata in protocol response
@ -1008,7 +1043,7 @@ class TestContextInjection:
return f"Request {ctx.request_id}: {x}"
mcp.add_tool(tool_with_context)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("tool_with_context", {"x": 42})
assert len(result.content) == 1
content = result.content[0]
@ -1026,7 +1061,7 @@ class TestContextInjection:
return f"Async request {ctx.request_id}: {x}"
mcp.add_tool(async_tool)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("async_tool", {"x": 42})
assert len(result.content) == 1
content = result.content[0]
@ -1049,7 +1084,7 @@ class TestContextInjection:
mcp.add_tool(logging_tool)
with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("logging_tool", {"msg": "test"})
assert len(result.content) == 1
content = result.content[0]
@ -1091,7 +1126,7 @@ class TestContextInjection:
return x * 2
mcp.add_tool(no_context)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("no_context", {"x": 21})
assert len(result.content) == 1
content = result.content[0]
@ -1115,7 +1150,7 @@ class TestContextInjection:
r = r_list[0]
return f"Read resource: {r.content} with mime type {r.mime_type}"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.call_tool("tool_with_resource", {})
assert len(result.content) == 1
content = result.content[0]
@ -1141,8 +1176,13 @@ class TestContextInjection:
assert template.context_kwarg == "ctx"
# Test via client
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://context/test")
async with Client(mcp) as client:
result = await client.read_resource("resource://context/test")
assert len(result.contents) == 1
content = result.contents[0]
assert isinstance(content, TextResourceContents)
@ -1166,8 +1206,13 @@ class TestContextInjection:
assert template.context_kwarg is None
# Test via client
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://nocontext/test")
async with Client(mcp) as client:
result = await client.read_resource("resource://nocontext/test")
assert len(result.contents) == 1
content = result.contents[0]
assert isinstance(content, TextResourceContents)
@ -1191,8 +1236,13 @@ class TestContextInjection:
assert template.context_kwarg == "my_ctx"
# Test via client
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.read_resource("resource://custom/123")
async with Client(mcp) as client:
result = await client.read_resource("resource://custom/123")
assert len(result.contents) == 1
content = result.contents[0]
assert isinstance(content, TextResourceContents)
@ -1214,7 +1264,7 @@ class TestContextInjection:
assert len(prompts) == 1
# Test via client
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
# Try calling without passing ctx explicitly
result = await client.get_prompt("prompt_with_ctx", {"text": "test"})
# If this succeeds, check if context was injected
@ -1234,7 +1284,7 @@ class TestContextInjection:
return f"Prompt '{text}' works"
# Test via client
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("prompt_no_ctx", {"text": "test"})
assert len(result.messages) == 1
message = result.messages[0]
@ -1313,7 +1363,7 @@ class TestServerPrompts:
def fn(name: str, optional: str = "default") -> str: # pragma: no cover
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.list_prompts()
assert result.prompts is not None
assert len(result.prompts) == 1
@ -1335,7 +1385,7 @@ class TestServerPrompts:
def fn(name: str) -> str:
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("fn", {"name": "World"})
assert len(result.messages) == 1
message = result.messages[0]
@ -1353,7 +1403,7 @@ class TestServerPrompts:
def fn(name: str) -> str:
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("fn", {"name": "World"})
assert result.description == "Test prompt description"
@ -1366,7 +1416,7 @@ class TestServerPrompts:
def fn(name: str) -> str:
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("fn", {"name": "World"})
assert result.description == ""
@ -1380,7 +1430,7 @@ class TestServerPrompts:
"""This is the function docstring."""
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("fn", {"name": "World"})
assert result.description == "This is the function docstring."
@ -1402,7 +1452,7 @@ class TestServerPrompts:
)
)
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
result = await client.get_prompt("fn")
assert len(result.messages) == 1
message = result.messages[0]
@ -1418,7 +1468,7 @@ class TestServerPrompts:
async def test_get_unknown_prompt(self):
"""Test error when getting unknown prompt."""
mcp = FastMCP()
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
with pytest.raises(McpError, match="Unknown prompt"):
await client.get_prompt("unknown")
@ -1431,7 +1481,7 @@ class TestServerPrompts:
def prompt_fn(name: str) -> str: # pragma: no cover
return f"Hello, {name}!"
async with client_session(mcp._mcp_server) as client:
async with Client(mcp) as client:
with pytest.raises(McpError, match="Missing required arguments"):
await client.get_prompt("prompt_fn")

View file

@ -2,9 +2,9 @@
import pytest
from mcp import Client
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.resources import FunctionResource
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.shared.metadata_utils import get_display_name
from mcp.types import Prompt, Resource, ResourceTemplate, Tool, ToolAnnotations
@ -24,8 +24,9 @@ async def test_server_name_title_description_version():
assert mcp.version == "1.0"
# Start server and connect client
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
init_result = await client.initialize()
async with Client(mcp) as client:
# Access initialization result from session
init_result = await client.session.initialize()
assert init_result.server_info.name == "TestServer"
assert init_result.server_info.title == "Test Server Title"
assert init_result.server_info.description == "This is a test server description."
@ -60,9 +61,7 @@ async def test_tool_title_precedence():
return message
# Start server and connect client
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
await client.initialize()
async with Client(mcp) as client:
# List tools
tools_result = await client.list_tools()
tools = {tool.name: tool for tool in tools_result.tools}
@ -104,9 +103,7 @@ async def test_prompt_title():
return f"Tell me about {topic}"
# Start server and connect client
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
await client.initialize()
async with Client(mcp) as client:
# List prompts
prompts_result = await client.list_prompts()
prompts = {prompt.name: prompt for prompt in prompts_result.prompts}
@ -164,9 +161,7 @@ async def test_resource_title():
return f"Data for {id}"
# Start server and connect client
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
await client.initialize()
async with Client(mcp) as client:
# List resources
resources_result = await client.list_resources()
resources = {str(res.uri): res for res in resources_result.resources}

View file

@ -4,13 +4,12 @@ import anyio
import pytest
from pydantic import BaseModel, Field
from mcp import types
from mcp import Client, types
from mcp.client.session import ClientSession
from mcp.server.elicitation import CancelledElicitation, DeclinedElicitation, elicit_url
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.shared.context import RequestContext
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import ElicitRequestParams, ElicitResult, TextContent
@ -37,12 +36,8 @@ async def test_url_elicitation_accept():
assert params.message == "Please provide your API key to continue."
return ElicitResult(action="accept")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("request_api_key", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("request_api_key", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "User accept"
@ -67,12 +62,8 @@ async def test_url_elicitation_decline():
assert params.mode == "url"
return ElicitResult(action="decline")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("oauth_flow", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("oauth_flow", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "User decline authorization"
@ -97,12 +88,8 @@ async def test_url_elicitation_cancel():
assert params.mode == "url"
return ElicitResult(action="cancel")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("payment_flow", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("payment_flow", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "User cancel payment"
@ -127,12 +114,8 @@ async def test_url_elicitation_helper_function():
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="accept")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("setup_credentials", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("setup_credentials", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "AcceptedUrlElicitation"
@ -165,12 +148,8 @@ async def test_url_no_content_in_response():
# Return without content - this is correct for URL mode
return ElicitResult(action="accept")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("check_url_response", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("check_url_response", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert "Content: None" in result.content[0].text
@ -200,12 +179,8 @@ async def test_form_mode_still_works():
assert params.requested_schema is not None
return ElicitResult(action="accept", content={"name": "Alice"})
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("ask_name", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("ask_name", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Hello, Alice!"
@ -235,12 +210,8 @@ async def test_elicit_complete_notification():
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="accept") # pragma: no cover
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("trigger_elicitation", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("trigger_elicitation", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Elicitation completed"
@ -296,12 +267,8 @@ async def test_elicit_url_typed_results():
async def decline_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="decline")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=decline_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("test_decline", {})
async with Client(mcp, elicitation_callback=decline_callback) as client:
result = await client.call_tool("test_decline", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Declined"
@ -310,12 +277,8 @@ async def test_elicit_url_typed_results():
async def cancel_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="cancel")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=cancel_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("test_cancel", {})
async with Client(mcp, elicitation_callback=cancel_callback) as client:
result = await client.call_tool("test_cancel", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Cancelled"
@ -347,12 +310,8 @@ async def test_deprecated_elicit_method():
assert params.requested_schema is not None
return ElicitResult(action="accept", content={"email": "test@example.com"})
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("use_deprecated_elicit", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("use_deprecated_elicit", {})
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Email: test@example.com"
@ -378,10 +337,7 @@ async def test_ctx_elicit_url_convenience_method():
assert params.elicitation_id == "ctx-test-001"
return ElicitResult(action="accept")
async with create_connected_server_and_client_session(
mcp._mcp_server, elicitation_callback=elicitation_callback
) as client_session:
await client_session.initialize()
result = await client_session.call_tool("direct_elicit_url", {})
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
result = await client.call_tool("direct_elicit_url", {})
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Result: accept"

View file

@ -2,11 +2,10 @@
import pytest
from mcp import types
from mcp import Client, types
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
from mcp.shared.memory import create_connected_server_and_client_session
@pytest.mark.anyio
@ -28,12 +27,10 @@ async def test_url_elicitation_error_thrown_from_tool():
]
)
async with create_connected_server_and_client_session(mcp._mcp_server) as client_session:
await client_session.initialize()
async with Client(mcp) as client:
# Call the tool - it should raise McpError with URL_ELICITATION_REQUIRED code
with pytest.raises(McpError) as exc_info:
await client_session.call_tool("connect_service", {"service_name": "github"})
await client.call_tool("connect_service", {"service_name": "github"})
# Verify the error details
error = exc_info.value.error
@ -74,12 +71,10 @@ async def test_url_elicitation_error_from_error():
]
)
async with create_connected_server_and_client_session(mcp._mcp_server) as client_session:
await client_session.initialize()
async with Client(mcp) as client:
# Call the tool and catch the error
with pytest.raises(McpError) as exc_info:
await client_session.call_tool("multi_auth", {})
await client.call_tool("multi_auth", {})
# Reconstruct the typed error
mcp_error = exc_info.value
@ -102,11 +97,9 @@ async def test_normal_exceptions_still_return_error_result():
async def failing_tool(ctx: Context[ServerSession, None]) -> str:
raise ValueError("Something went wrong")
async with create_connected_server_and_client_session(mcp._mcp_server) as client_session:
await client_session.initialize()
async with Client(mcp) as client:
# Normal exceptions should be returned as error results, not McpError
result = await client_session.call_tool("failing_tool", {})
result = await client.call_tool("failing_tool", {})
assert result.is_error is True
assert len(result.content) == 1
assert isinstance(result.content[0], types.TextContent)

View file

@ -6,9 +6,10 @@ import anyio
import pytest
import mcp.types as types
from mcp.client._memory import InMemoryTransport
from mcp.client.session import ClientSession
from mcp.server.lowlevel.server import Server
from mcp.shared.exceptions import McpError
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import (
CallToolRequest,
CallToolRequestParams,
@ -54,57 +55,61 @@ async def test_server_remains_functional_after_cancel():
return [types.TextContent(type="text", text=f"Call number: {call_count}")]
raise ValueError(f"Unknown tool: {name}") # pragma: no cover
async with create_connected_server_and_client_session(server) as client:
# First request (will be cancelled)
async def first_request():
try:
await client.send_request(
ClientRequest(
CallToolRequest(
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
pytest.fail("First request should have been cancelled") # pragma: no cover
except McpError:
pass # Expected
transport = InMemoryTransport(server)
async with transport.connect() as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as client:
await client.initialize()
# Start first request
async with anyio.create_task_group() as tg:
tg.start_soon(first_request)
# Wait for it to start
await ev_first_call.wait()
# Cancel it
assert first_request_id is not None
await client.send_notification(
ClientNotification(
CancelledNotification(
params=CancelledNotificationParams(
request_id=first_request_id,
reason="Testing server recovery",
# First request (will be cancelled)
async def first_request():
try:
await client.send_request(
ClientRequest(
CallToolRequest(
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
pytest.fail("First request should have been cancelled") # pragma: no cover
except McpError:
pass # Expected
# Start first request
async with anyio.create_task_group() as tg:
tg.start_soon(first_request)
# Wait for it to start
await ev_first_call.wait()
# Cancel it
assert first_request_id is not None
await client.send_notification(
ClientNotification(
CancelledNotification(
params=CancelledNotificationParams(
request_id=first_request_id,
reason="Testing server recovery",
),
)
)
)
# Second request (should work normally)
result = await client.send_request(
ClientRequest(
CallToolRequest(
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
# Second request (should work normally)
result = await client.send_request(
ClientRequest(
CallToolRequest(
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
# Verify second request completed successfully
assert len(result.content) == 1
# Type narrowing for pyright
content = result.content[0]
assert content.type == "text"
assert isinstance(content, types.TextContent)
assert content.text == "Call number: 2"
assert call_count == 2
# Verify second request completed successfully
assert len(result.content) == 1
# Type narrowing for pyright
content = result.content[0]
assert content.type == "text"
assert isinstance(content, types.TextContent)
assert content.text == "Call number: 2"
assert call_count == 2

View file

@ -6,8 +6,8 @@ from typing import Any
import pytest
from mcp import Client
from mcp.server.lowlevel import Server
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import (
Completion,
CompletionArgument,
@ -38,7 +38,7 @@ async def test_completion_handler_receives_context():
# Return test completion
return Completion(values=["test-completion"], total=1, has_more=False)
async with create_connected_server_and_client_session(server) as client:
async with Client(server) as client:
# Test with context
result = await client.complete(
ref=ResourceTemplateReference(type="ref/resource", uri="test://resource/{param}"),
@ -70,7 +70,7 @@ async def test_completion_backward_compatibility():
return Completion(values=["no-context-completion"], total=1, has_more=False)
async with create_connected_server_and_client_session(server) as client:
async with Client(server) as client:
# Test without context
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="test-prompt"), argument={"name": "arg", "value": "val"}
@ -109,7 +109,7 @@ async def test_dependent_completion_scenario():
return Completion(values=[], total=0, has_more=False) # pragma: no cover
async with create_connected_server_and_client_session(server) as client:
async with Client(server) as client:
# First, complete database
db_result = await client.complete(
ref=ResourceTemplateReference(type="ref/resource", uri="db://{database}/{table}"),
@ -160,7 +160,7 @@ async def test_completion_error_on_missing_context():
return Completion(values=[], total=0, has_more=False) # pragma: no cover
async with create_connected_server_and_client_session(server) as client:
async with Client(server) as client:
# Try to complete table without database context - should raise error
with pytest.raises(Exception) as exc_info:
await client.complete(