Four read-only, service-token-protected MCP provider endpoints (operations summary, attention vehicles, vehicle details, knowledge search facade). Shared-secret auth reusing the M4 callback pattern. Service-request audit trail for every call. Extracted shared operations-summary logic out of the dashboard router to avoid duplicating retrieval logic. 66 backend tests passing, ruff clean. Verified all four endpoints and audit trail directly via curl against the live stack (no live MCP Hub instance available in this environment).
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
from app.core.config import get_settings
|
|
|
|
|
|
def _headers(token: str | None = None, client_id: str = "test-mcp-client"):
|
|
settings = get_settings()
|
|
return {
|
|
"X-Service-Token": token if token is not None else settings.mcp_hub_service_token,
|
|
"X-Client-Id": client_id,
|
|
}
|
|
|
|
|
|
def test_operations_summary_requires_service_token(client):
|
|
response = client.get(
|
|
"/api/v1/integrations/mcp/operations-summary", headers=_headers(token="wrong")
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_operations_summary_returns_metrics(client):
|
|
response = client.get("/api/v1/integrations/mcp/operations-summary", headers=_headers())
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert "metrics" in body
|
|
assert body["metrics"]["available"] >= 0
|
|
|
|
|
|
def test_attention_vehicles_filters_by_severity(client):
|
|
response = client.get(
|
|
"/api/v1/integrations/mcp/attention-vehicles",
|
|
params={"minimum_severity": "high", "limit": 50},
|
|
headers=_headers(),
|
|
)
|
|
assert response.status_code == 200
|
|
rows = response.json()
|
|
assert all(r["severity"] == "high" for r in rows)
|
|
|
|
|
|
def test_attention_vehicles_respects_limit(client):
|
|
response = client.get(
|
|
"/api/v1/integrations/mcp/attention-vehicles",
|
|
params={"minimum_severity": "low", "limit": 2},
|
|
headers=_headers(),
|
|
)
|
|
assert response.status_code == 200
|
|
assert len(response.json()) <= 2
|
|
|
|
|
|
def test_vehicle_details_known_ref(client):
|
|
response = client.get(
|
|
"/api/v1/integrations/mcp/vehicles/MO-016", headers=_headers()
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["public_ref"] == "MO-016"
|
|
assert "registration_number" not in body # narrow read-only view, not the full record
|
|
|
|
|
|
def test_vehicle_details_unknown_ref_is_404(client):
|
|
response = client.get(
|
|
"/api/v1/integrations/mcp/vehicles/MO-999", headers=_headers()
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_search_knowledge_grounded_and_respects_max_sources(client):
|
|
response = client.post(
|
|
"/api/v1/integrations/mcp/search-knowledge",
|
|
json={"question": "What must I do when a vehicle returns with damage?", "max_sources": 1},
|
|
headers=_headers(),
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["evidence_state"] == "grounded"
|
|
assert len(body["sources"]) == 1
|
|
|
|
|
|
def test_mcp_tool_requests_are_audited(client, ops_client):
|
|
client.get("/api/v1/integrations/mcp/operations-summary", headers=_headers(client_id="probe-1"))
|
|
events = ops_client.get("/api/v1/audit", params={"action": "mcp_tool_request"}).json()
|
|
assert len(events) >= 1
|
|
assert events[0]["actor_type"] == "service"
|
|
|
|
|
|
def test_no_write_endpoints_exist_under_mcp_namespace(client):
|
|
for method, path in [
|
|
("post", "/api/v1/integrations/mcp/vehicles/MO-016"),
|
|
("put", "/api/v1/integrations/mcp/vehicles/MO-016"),
|
|
("delete", "/api/v1/integrations/mcp/vehicles/MO-016"),
|
|
]:
|
|
response = getattr(client, method)(path, headers=_headers())
|
|
assert response.status_code in (404, 405)
|