188 lines
6.9 KiB
Python
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
|