"""Integration tests for MCPServer server functionality. These tests validate the proper functioning of MCPServer features using focused, single-feature example servers over an in-memory transport. """ # TODO(Marcelo): The `examples` package is not being imported as package. We need to solve this. # pyright: reportUnknownMemberType=false # pyright: reportMissingImports=false # pyright: reportUnknownVariableType=false # pyright: reportUnknownArgumentType=false import json import pytest from inline_snapshot import snapshot from mcp_types import ( ClientResult, CreateMessageRequestParams, CreateMessageResult, ElicitRequestParams, ElicitResult, GetPromptResult, LoggingMessageNotification, LoggingMessageNotificationParams, NotificationParams, ProgressNotification, ProgressNotificationParams, PromptReference, ReadResourceResult, ResourceListChangedNotification, ResourceTemplateReference, ServerNotification, ServerRequest, TextContent, TextResourceContents, ToolListChangedNotification, ) from examples.snippets.servers import ( basic_prompt, basic_resource, basic_tool, completion, elicitation, mcpserver_quickstart, notifications, sampling, structured_output, tool_progress, ) from mcp.client import Client, ClientRequestContext from mcp.shared.session import RequestResponder pytestmark = pytest.mark.anyio class NotificationCollector: """Collects notifications from the server for testing.""" def __init__(self): self.progress_notifications: list[ProgressNotificationParams] = [] self.log_messages: list[LoggingMessageNotificationParams] = [] self.resource_notifications: list[NotificationParams | None] = [] self.tool_notifications: list[NotificationParams | None] = [] async def handle_generic_notification( self, message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception ) -> None: """Handle any server notification and route to appropriate handler.""" if isinstance(message, ServerNotification): # pragma: no branch if isinstance(message, ProgressNotification): self.progress_notifications.append(message.params) elif isinstance(message, LoggingMessageNotification): self.log_messages.append(message.params) elif isinstance(message, ResourceListChangedNotification): self.resource_notifications.append(message.params) elif isinstance(message, ToolListChangedNotification): # pragma: no cover self.tool_notifications.append(message.params) async def sampling_callback(context: ClientRequestContext, params: CreateMessageRequestParams) -> CreateMessageResult: """Sampling callback for tests.""" return CreateMessageResult( role="assistant", content=TextContent( type="text", text="This is a simulated LLM response for testing", ), model="test-model", ) async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams): """Elicitation callback for tests.""" # For restaurant booking test if "No tables available" in params.message: return ElicitResult( action="accept", content={"checkAlternative": True, "alternativeDate": "2024-12-26"}, ) else: # pragma: no cover return ElicitResult(action="decline") async def test_basic_tools() -> None: """Test basic tool functionality.""" async with Client(basic_tool.mcp) as client: assert client.server_capabilities.tools is not None # Test sum tool tool_result = await client.call_tool("sum", {"a": 5, "b": 3}) assert len(tool_result.content) == 1 assert isinstance(tool_result.content[0], TextContent) assert tool_result.content[0].text == "8" # Test weather tool weather_result = await client.call_tool("get_weather", {"city": "London"}) assert len(weather_result.content) == 1 assert isinstance(weather_result.content[0], TextContent) assert "Weather in London: 22degreesC" in weather_result.content[0].text async def test_basic_resources() -> None: """Test basic resource functionality.""" async with Client(basic_resource.mcp) as client: assert client.server_capabilities.resources is not None # Test document resource doc_content = await client.read_resource("file://documents/readme") assert isinstance(doc_content, ReadResourceResult) assert len(doc_content.contents) == 1 assert isinstance(doc_content.contents[0], TextResourceContents) assert "Content of readme" in doc_content.contents[0].text # Test settings resource settings_content = await client.read_resource("config://settings") assert isinstance(settings_content, ReadResourceResult) assert len(settings_content.contents) == 1 assert isinstance(settings_content.contents[0], TextResourceContents) settings_json = json.loads(settings_content.contents[0].text) assert settings_json["theme"] == "dark" assert settings_json["language"] == "en" async def test_basic_prompts() -> None: """Test basic prompt functionality.""" async with Client(basic_prompt.mcp) as client: assert client.server_capabilities.prompts is not None # Test review_code prompt prompts = await client.list_prompts() review_prompt = next((p for p in prompts.prompts if p.name == "review_code"), None) assert review_prompt is not None prompt_result = await client.get_prompt("review_code", {"code": "def hello():\n print('Hello')"}) assert isinstance(prompt_result, GetPromptResult) assert len(prompt_result.messages) == 1 assert isinstance(prompt_result.messages[0].content, TextContent) assert "Please review this code:" in prompt_result.messages[0].content.text assert "def hello():" in prompt_result.messages[0].content.text # Test debug_error prompt debug_result = await client.get_prompt( "debug_error", {"error": "TypeError: 'NoneType' object is not subscriptable"} ) assert isinstance(debug_result, GetPromptResult) assert len(debug_result.messages) == 3 assert debug_result.messages[0].role == "user" assert isinstance(debug_result.messages[0].content, TextContent) assert "I'm seeing this error:" in debug_result.messages[0].content.text assert debug_result.messages[1].role == "user" assert isinstance(debug_result.messages[1].content, TextContent) assert "TypeError" in debug_result.messages[1].content.text assert debug_result.messages[2].role == "assistant" assert isinstance(debug_result.messages[2].content, TextContent) assert "I'll help debug that" in debug_result.messages[2].content.text async def test_tool_progress() -> None: """Test tool progress reporting.""" collector = NotificationCollector() async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception): await collector.handle_generic_notification(message) if isinstance(message, Exception): # pragma: no cover raise message async with Client(tool_progress.mcp, message_handler=message_handler, mode="legacy") as client: # Test progress callback progress_updates = [] async def progress_callback(progress: float, total: float | None, message: str | None) -> None: progress_updates.append((progress, total, message)) # Call tool with progress steps = 3 tool_result = await client.call_tool( "long_running_task", {"task_name": "Test Task", "steps": steps}, progress_callback=progress_callback, ) assert tool_result.content == snapshot([TextContent(text="Task 'Test Task' completed")]) # Verify progress updates assert len(progress_updates) == steps for i, (progress, total, message) in enumerate(progress_updates): expected_progress = (i + 1) / steps assert abs(progress - expected_progress) < 0.01 assert total == 1.0 assert f"Step {i + 1}/{steps}" in message # Verify log messages assert len(collector.log_messages) > 0 async def test_sampling() -> None: """Test sampling (LLM interaction) functionality.""" async with Client(sampling.mcp, sampling_callback=sampling_callback, mode="legacy") as client: assert client.server_capabilities.tools is not None # Test sampling tool sampling_result = await client.call_tool("generate_poem", {"topic": "nature"}) assert len(sampling_result.content) == 1 assert isinstance(sampling_result.content[0], TextContent) assert "This is a simulated LLM response" in sampling_result.content[0].text async def test_elicitation() -> None: """Test elicitation (user interaction) functionality.""" async with Client(elicitation.mcp, elicitation_callback=elicitation_callback, mode="legacy") as client: # Test booking with unavailable date (triggers elicitation) booking_result = await client.call_tool( "book_table", { "date": "2024-12-25", # Unavailable date "time": "19:00", "party_size": 4, }, ) assert len(booking_result.content) == 1 assert isinstance(booking_result.content[0], TextContent) assert "[SUCCESS] Booked for 2024-12-26" in booking_result.content[0].text # Test booking with available date (no elicitation) booking_result = await client.call_tool( "book_table", { "date": "2024-12-20", # Available date "time": "20:00", "party_size": 2, }, ) assert len(booking_result.content) == 1 assert isinstance(booking_result.content[0], TextContent) assert "[SUCCESS] Booked for 2024-12-20 at 20:00" in booking_result.content[0].text async def test_notifications() -> None: """Test notifications and logging functionality.""" collector = NotificationCollector() async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception): await collector.handle_generic_notification(message) if isinstance(message, Exception): # pragma: no cover raise message async with Client(notifications.mcp, message_handler=message_handler, mode="legacy") as client: # Call tool that generates notifications tool_result = await client.call_tool("process_data", {"data": "test_data"}) assert len(tool_result.content) == 1 assert isinstance(tool_result.content[0], TextContent) assert "Processed: test_data" in tool_result.content[0].text # Verify log messages at different levels assert len(collector.log_messages) >= 4 log_levels = {msg.level for msg in collector.log_messages} assert "debug" in log_levels assert "info" in log_levels assert "warning" in log_levels assert "error" in log_levels # Verify resource list changed notification assert len(collector.resource_notifications) > 0 async def test_completion() -> None: """Test completion (autocomplete) functionality.""" async with Client(completion.mcp) as client: assert client.server_capabilities.resources is not None assert client.server_capabilities.prompts is not None # Test resource completion completion_result = await client.complete( ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"), argument={"name": "repo", "value": ""}, context_arguments={"owner": "modelcontextprotocol"}, ) assert completion_result is not None assert hasattr(completion_result, "completion") assert completion_result.completion is not None assert len(completion_result.completion.values) == 3 assert "python-sdk" in completion_result.completion.values assert "typescript-sdk" in completion_result.completion.values assert "specification" in completion_result.completion.values # Test prompt completion completion_result = await client.complete( ref=PromptReference(type="ref/prompt", name="review_code"), argument={"name": "language", "value": "py"}, ) assert completion_result is not None assert hasattr(completion_result, "completion") assert completion_result.completion is not None assert "python" in completion_result.completion.values assert all(lang.startswith("py") for lang in completion_result.completion.values) async def test_mcpserver_quickstart() -> None: """Test MCPServer quickstart example.""" async with Client(mcpserver_quickstart.mcp) as client: # Test add tool tool_result = await client.call_tool("add", {"a": 10, "b": 20}) assert len(tool_result.content) == 1 assert isinstance(tool_result.content[0], TextContent) assert tool_result.content[0].text == "30" # Test greeting resource directly resource_result = await client.read_resource("greeting://Alice") assert len(resource_result.contents) == 1 assert isinstance(resource_result.contents[0], TextResourceContents) assert resource_result.contents[0].text == "Hello, Alice!" async def test_structured_output() -> None: """Test structured output functionality.""" async with Client(structured_output.mcp) as client: # Test get_weather tool weather_result = await client.call_tool("get_weather", {"city": "New York"}) assert len(weather_result.content) == 1 assert isinstance(weather_result.content[0], TextContent) # Check that the result contains expected weather data result_text = weather_result.content[0].text assert "22.5" in result_text # temperature assert "sunny" in result_text # condition assert "45" in result_text # humidity assert "5.2" in result_text # wind_speed