fischer-agentkit/tests/unit/test_mcp_server.py

188 lines
6.9 KiB
Python

"""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