"""Tests for MCPServer - FastAPI application exposing tools via HTTP endpoints""" import pytest import httpx from agentkit.mcp.server import MCPServer from agentkit.tools.function_tool import FunctionTool from agentkit.tools.registry import ToolRegistry # ── Helper functions ────────────────────────────────────── async def add_numbers(a: int, b: int) -> dict: return {"sum": a + b} async def failing_tool() -> dict: raise RuntimeError("tool execution failed") # ── Fixtures ────────────────────────────────────────────── @pytest.fixture def registry_with_tools(): """ToolRegistry with a couple of registered tools.""" registry = ToolRegistry() registry.register( FunctionTool(name="add", description="Add two numbers", func=add_numbers) ) registry.register( FunctionTool(name="fail", description="Always fails", func=failing_tool) ) return registry @pytest.fixture def empty_registry(): """Empty ToolRegistry.""" return ToolRegistry() @pytest.fixture def client_factory(): """Factory that creates an httpx.AsyncClient for a given MCPServer.""" def _factory(server: MCPServer) -> httpx.AsyncClient: app = server.get_app() transport = httpx.ASGITransport(app=app) return httpx.AsyncClient(transport=transport, base_url="http://test") return _factory # ── Health endpoint ─────────────────────────────────────── class TestHealthEndpoint: async def test_health_returns_ok(self, client_factory): server = MCPServer() async with client_factory(server) as client: resp = await client.get("/health") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} # ── List tools endpoint ────────────────────────────────── class TestListTools: async def test_list_tools_empty_registry(self, client_factory, empty_registry): server = MCPServer(tool_registry=empty_registry) async with client_factory(server) as client: resp = await client.get("/tools/list") assert resp.status_code == 200 body = resp.json() assert body == {"tools": []} async def test_list_tools_no_registry(self, client_factory): server = MCPServer() async with client_factory(server) as client: resp = await client.get("/tools/list") assert resp.status_code == 200 body = resp.json() assert body == {"tools": []} async def test_list_tools_with_registered_tools(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.get("/tools/list") assert resp.status_code == 200 body = resp.json() tools = body["tools"] assert len(tools) == 2 names = {t["name"] for t in tools} assert names == {"add", "fail"} # Verify tool shape for t in tools: assert "name" in t assert "description" in t assert "inputSchema" in t async def test_list_tools_includes_input_schema(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.get("/tools/list") body = resp.json() add_tool = next(t for t in body["tools"] if t["name"] == "add") assert "properties" in add_tool["inputSchema"] # ── Call tool endpoint ─────────────────────────────────── class TestCallTool: async def test_call_tool_success(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.post("/tools/call", json={"name": "add", "arguments": {"a": 3, "b": 5}}) assert resp.status_code == 200 body = resp.json() assert "content" in body assert body["content"][0]["type"] == "text" assert "8" in body["content"][0]["text"] async def test_call_tool_missing_name(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.post("/tools/call", json={"arguments": {"a": 1}}) assert resp.status_code == 200 body = resp.json() assert "error" in body async def test_call_tool_no_registry(self, client_factory): server = MCPServer() async with client_factory(server) as client: resp = await client.post("/tools/call", json={"name": "add", "arguments": {}}) assert resp.status_code == 200 body = resp.json() assert "error" in body async def test_call_tool_execution_error(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.post("/tools/call", json={"name": "fail", "arguments": {}}) assert resp.status_code == 200 body = resp.json() assert body.get("isError") is True assert "content" in body assert "tool execution failed" in body["content"][0]["text"] async def test_call_tool_nonexistent_tool(self, client_factory, registry_with_tools): server = MCPServer(tool_registry=registry_with_tools) async with client_factory(server) as client: resp = await client.post("/tools/call", json={"name": "nonexistent", "arguments": {}}) assert resp.status_code == 200 body = resp.json() assert body.get("isError") is True # ── Server construction ────────────────────────────────── class TestMCPServerConstruction: def test_default_host_and_port(self): server = MCPServer() assert server._host == "0.0.0.0" assert server._port == 8080 def test_custom_host_and_port(self): server = MCPServer(host="127.0.0.1", port=9090) assert server._host == "127.0.0.1" assert server._port == 9090 def test_get_app_creates_app(self): server = MCPServer() app = server.get_app() assert app is not None # Second call returns same instance assert server.get_app() is app def test_get_app_lazy_creation(self): server = MCPServer() assert server._app is None server.get_app() assert server._app is not None