207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
"""Tests for MCP servers — tools, resources, prompts via the MCP protocol."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from fastmcp import Client
|
|
|
|
|
|
def _client(module: str) -> Client:
|
|
return Client({"mcpServers": {"default": {
|
|
"command": "uv",
|
|
"args": ["run", "python", "-m", module],
|
|
}}})
|
|
|
|
|
|
def _parse_result(result) -> any:
|
|
"""Parse a fastmcp CallToolResult into Python data."""
|
|
if hasattr(result, "content"):
|
|
texts = [c.text for c in result.content if hasattr(c, "text")]
|
|
elif isinstance(result, list):
|
|
texts = [c.text for c in result if hasattr(c, "text")]
|
|
else:
|
|
return result
|
|
|
|
if not texts:
|
|
return None
|
|
combined = texts[0] if len(texts) == 1 else "[" + ",".join(texts) + "]"
|
|
try:
|
|
return json.loads(combined)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return combined
|
|
|
|
|
|
class TestSharedServer:
|
|
@pytest.mark.asyncio
|
|
async def test_list_tools(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
tools = await c.list_tools()
|
|
names = [t.name for t in tools]
|
|
assert "get_flight_status" in names
|
|
assert "get_flight_details" in names
|
|
assert "get_irregular_ops" in names
|
|
assert "get_route_weather" in names
|
|
assert "get_hub_forecasts" in names
|
|
assert "get_airport_status" in names
|
|
assert "get_airport_congestion" in names
|
|
assert "get_maintenance_flags" in names
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flight_status_found(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
result = await c.call_tool("get_flight_status", {"flight_id": "UA432"})
|
|
data = _parse_result(result)
|
|
assert data["flight_id"] == "UA432"
|
|
assert data["status"] == "DELAYED"
|
|
assert data["delay_minutes"] == 55
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flight_status_not_found(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
result = await c.call_tool("get_flight_status", {"flight_id": "FAKE999"})
|
|
data = _parse_result(result)
|
|
assert "error" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_irregular_ops(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
result = await c.call_tool("get_irregular_ops", {"hub": "ORD"})
|
|
data = _parse_result(result)
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 4
|
|
for f in data:
|
|
assert f["irrop_type"] in ("DELAYED", "CANCELLED", "DIVERTED")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_maintenance_flags(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
result = await c.call_tool("get_maintenance_flags", {"aircraft_tail": "N78501"})
|
|
data = _parse_result(result)
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
assert "system" in data[0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_maintenance_flags_empty(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
result = await c.call_tool("get_maintenance_flags", {"aircraft_tail": "FAKE"})
|
|
data = _parse_result(result)
|
|
# Empty list may come back as [] or as "[]" string
|
|
if isinstance(data, list):
|
|
assert len(data) == 0
|
|
else:
|
|
assert data == "[]" or data is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_prompts(self):
|
|
async with _client("mcp_servers.shared") as c:
|
|
prompts = await c.list_prompts()
|
|
names = [p.name for p in prompts]
|
|
assert "delay_explainer" in names
|
|
|
|
|
|
class TestOpsServer:
|
|
@pytest.mark.asyncio
|
|
async def test_list_tools(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
tools = await c.list_tools()
|
|
names = [t.name for t in tools]
|
|
assert "get_crew_notes" in names
|
|
assert "get_crew_duty_status" in names
|
|
assert "get_pending_rebookings" in names
|
|
assert "generate_narrative" in names
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_crew_notes(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
result = await c.call_tool("get_crew_notes", {"flight_id": "UA432"})
|
|
data = _parse_result(result)
|
|
assert isinstance(data, list)
|
|
assert len(data) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_crew_duty_status(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
result = await c.call_tool(
|
|
"get_crew_duty_status",
|
|
{"crew_ids": ["CR-1001", "CR-1003"]}
|
|
)
|
|
data = _parse_result(result)
|
|
assert isinstance(data, list)
|
|
assert len(data) == 2
|
|
for c_data in data:
|
|
assert "hours_until_limit" in c_data
|
|
assert "at_risk" in c_data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_crew_at_risk_detection(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
result = await c.call_tool(
|
|
"get_crew_duty_status",
|
|
{"crew_ids": ["CR-1003"]}
|
|
)
|
|
data = _parse_result(result)
|
|
crew = data[0] if isinstance(data, list) else data
|
|
assert crew["at_risk"] is True
|
|
assert crew["hours_until_limit"] <= 2.0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_rebookings(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
result = await c.call_tool("get_pending_rebookings", {"hub": "ORD"})
|
|
data = _parse_result(result)
|
|
assert isinstance(data, list)
|
|
assert len(data) > 0
|
|
urgencies = [r["urgency"] for r in data]
|
|
assert urgencies[0] == "HIGH"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_resources(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
resources = await c.list_resources()
|
|
uris = [str(r.uri) for r in resources]
|
|
assert "ops://crew/roster" in uris
|
|
assert "ops://handover/latest" in uris
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_prompts(self):
|
|
async with _client("mcp_servers.ops") as c:
|
|
prompts = await c.list_prompts()
|
|
names = [p.name for p in prompts]
|
|
assert "handover_brief" in names
|
|
|
|
|
|
class TestPassengerServer:
|
|
@pytest.mark.asyncio
|
|
async def test_list_tools(self):
|
|
async with _client("mcp_servers.passenger") as c:
|
|
tools = await c.list_tools()
|
|
names = [t.name for t in tools]
|
|
assert "generate_notification" in names
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_notification(self):
|
|
async with _client("mcp_servers.passenger") as c:
|
|
result = await c.call_tool("generate_notification", {
|
|
"context": {
|
|
"flight_id": "UA432",
|
|
"origin": "ORD",
|
|
"destination": "SFO",
|
|
"status": "DELAYED",
|
|
"delay_minutes": 55,
|
|
"delay_cause": "WEATHER",
|
|
"gate": "H14",
|
|
}
|
|
})
|
|
text = _parse_result(result)
|
|
assert "UA432" in text
|
|
assert "DELAYED" in text or "delayed" in text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_prompts(self):
|
|
async with _client("mcp_servers.passenger") as c:
|
|
prompts = await c.list_prompts()
|
|
names = [p.name for p in prompts]
|
|
assert "passenger_notification" in names
|