import logging from typing import Any import pytest from mcp_types import ( CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool, ) from mcp import Client from mcp.server import Server, ServerRequestContext def _make_server( tools: list[Tool], structured_content: dict[str, Any], ) -> Server: """Create a low-level server that returns the given structured_content for any tool call.""" async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: return ListToolsResult(tools=tools) async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: return CallToolResult( content=[TextContent(type="text", text="result")], structured_content=structured_content, ) return Server("test-server", on_list_tools=on_list_tools, on_call_tool=on_call_tool) @pytest.mark.anyio async def test_tool_structured_output_client_side_validation_basemodel(): """Test that client validates structured content against schema for BaseModel outputs""" output_schema = { "type": "object", "properties": {"name": {"type": "string", "title": "Name"}, "age": {"type": "integer", "title": "Age"}}, "required": ["name", "age"], "title": "UserOutput", } server = _make_server( tools=[ Tool( name="get_user", description="Get user data", input_schema={"type": "object"}, output_schema=output_schema, ) ], structured_content={"name": "John", "age": "invalid"}, # Invalid: age should be int ) async with Client(server) as client: with pytest.raises(RuntimeError) as exc_info: await client.call_tool("get_user", {}) assert "Invalid structured content returned by tool get_user" in str(exc_info.value) @pytest.mark.anyio async def test_tool_structured_output_client_side_validation_primitive(): """Test that client validates structured content for primitive outputs""" output_schema = { "type": "object", "properties": {"result": {"type": "integer", "title": "Result"}}, "required": ["result"], "title": "calculate_Output", } server = _make_server( tools=[ Tool( name="calculate", description="Calculate something", input_schema={"type": "object"}, output_schema=output_schema, ) ], structured_content={"result": "not_a_number"}, # Invalid: should be int ) async with Client(server) as client: with pytest.raises(RuntimeError) as exc_info: await client.call_tool("calculate", {}) assert "Invalid structured content returned by tool calculate" in str(exc_info.value) @pytest.mark.anyio async def test_tool_structured_output_client_side_validation_dict_typed(): """Test that client validates dict[str, T] structured content""" output_schema = {"type": "object", "additionalProperties": {"type": "integer"}, "title": "get_scores_Output"} server = _make_server( tools=[ Tool( name="get_scores", description="Get scores", input_schema={"type": "object"}, output_schema=output_schema, ) ], structured_content={"alice": "100", "bob": "85"}, # Invalid: values should be int ) async with Client(server) as client: with pytest.raises(RuntimeError) as exc_info: await client.call_tool("get_scores", {}) assert "Invalid structured content returned by tool get_scores" in str(exc_info.value) @pytest.mark.anyio async def test_tool_structured_output_client_side_validation_missing_required(): """Test that client validates missing required fields""" output_schema = { "type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}, "email": {"type": "string"}}, "required": ["name", "age", "email"], "title": "PersonOutput", } server = _make_server( tools=[ Tool( name="get_person", description="Get person data", input_schema={"type": "object"}, output_schema=output_schema, ) ], structured_content={"name": "John", "age": 30}, # Missing required 'email' ) async with Client(server) as client: with pytest.raises(RuntimeError) as exc_info: await client.call_tool("get_person", {}) assert "Invalid structured content returned by tool get_person" in str(exc_info.value) @pytest.mark.anyio async def test_tool_not_listed_warning(caplog: pytest.LogCaptureFixture): """Test that client logs warning when tool is not in list_tools but has output_schema""" async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: return ListToolsResult(tools=[]) async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: return CallToolResult( content=[TextContent(type="text", text="result")], structured_content={"result": 42}, ) server = Server("test-server", on_list_tools=on_list_tools, on_call_tool=on_call_tool) caplog.set_level(logging.WARNING) async with Client(server) as client: result = await client.call_tool("mystery_tool", {}) assert result.structured_content == {"result": 42} assert result.is_error is False assert "Tool mystery_tool not listed" in caplog.text