172 lines
5.7 KiB
Python
172 lines
5.7 KiB
Python
|
|
"""Tests for MCP Client plugin — server config CRUD, tool listing, execution.
|
||
|
|
|
||
|
|
Tests Task 5.17 acceptance criteria.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from tests.conftest import ORIGIN_HEADER
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC1: List MCP client servers (empty) ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac1_list_servers_empty(mcp_authed_client):
|
||
|
|
"""AC1: GET /api/v1/mcp-client/servers → 200 + empty list."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
resp = await client.get("/api/v1/mcp-client/servers", headers=ORIGIN_HEADER)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
assert resp.json() == []
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC2: Create MCP server config ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac2_create_server(mcp_authed_client):
|
||
|
|
"""AC2: POST /api/v1/mcp-client/servers → 201 + created config."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
resp = await client.post(
|
||
|
|
"/api/v1/mcp-client/servers",
|
||
|
|
json={"name": "Web Search MCP", "url": "http://localhost:9000", "api_token": "test-token", "enabled": True, "description": "External web search"},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
assert resp.status_code == 201
|
||
|
|
data = resp.json()
|
||
|
|
assert data["name"] == "Web Search MCP"
|
||
|
|
assert data["url"] == "http://localhost:9000"
|
||
|
|
assert data["api_token"] == "test-token"
|
||
|
|
assert data["enabled"] is True
|
||
|
|
assert data["description"] == "External web search"
|
||
|
|
assert "id" in data
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC3: Update MCP server config ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac3_update_server(mcp_authed_client):
|
||
|
|
"""AC3: PATCH /api/v1/mcp-client/servers/{id} → 200 + updated."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
# Create first
|
||
|
|
resp = await client.post(
|
||
|
|
"/api/v1/mcp-client/servers",
|
||
|
|
json={"name": "Test Server", "url": "http://localhost:8000", "enabled": True},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
server_id = resp.json()["id"]
|
||
|
|
|
||
|
|
# Update
|
||
|
|
resp = await client.patch(
|
||
|
|
f"/api/v1/mcp-client/servers/{server_id}",
|
||
|
|
json={"name": "Updated Server", "enabled": False},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
data = resp.json()
|
||
|
|
assert data["name"] == "Updated Server"
|
||
|
|
assert data["enabled"] is False
|
||
|
|
assert data["url"] == "http://localhost:8000" # unchanged
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC4: Delete MCP server config ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac4_delete_server(mcp_authed_client):
|
||
|
|
"""AC4: DELETE /api/v1/mcp-client/servers/{id} → 204."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
# Create first
|
||
|
|
resp = await client.post(
|
||
|
|
"/api/v1/mcp-client/servers",
|
||
|
|
json={"name": "Delete Me", "url": "http://localhost:7000", "enabled": True},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
server_id = resp.json()["id"]
|
||
|
|
|
||
|
|
# Delete
|
||
|
|
resp = await client.delete(f"/api/v1/mcp-client/servers/{server_id}", headers=ORIGIN_HEADER)
|
||
|
|
assert resp.status_code == 204
|
||
|
|
|
||
|
|
# Verify deleted
|
||
|
|
resp = await client.get("/api/v1/mcp-client/servers", headers=ORIGIN_HEADER)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
assert all(s["id"] != server_id for s in resp.json())
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC5: List servers after creating ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac5_list_servers_after_create(mcp_authed_client):
|
||
|
|
"""AC5: GET /api/v1/mcp-client/servers → 200 + list with created server."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/mcp-client/servers",
|
||
|
|
json={"name": "Server A", "url": "http://a:8000", "enabled": True},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/mcp-client/servers",
|
||
|
|
json={"name": "Server B", "url": "http://b:8000", "enabled": False},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
resp = await client.get("/api/v1/mcp-client/servers", headers=ORIGIN_HEADER)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
data = resp.json()
|
||
|
|
assert len(data) == 2
|
||
|
|
names = [s["name"] for s in data]
|
||
|
|
assert "Server A" in names
|
||
|
|
assert "Server B" in names
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC6: Unauthorized access is rejected ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac6_unauthorized_access(mcp_client_fixture):
|
||
|
|
"""AC6: GET /api/v1/mcp-client/servers without auth → 401."""
|
||
|
|
resp = await mcp_client_fixture.get("/api/v1/mcp-client/servers", headers=ORIGIN_HEADER)
|
||
|
|
assert resp.status_code == 401
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC7: Execute tool on non-existent server returns 404 ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac7_execute_on_nonexistent_server(mcp_authed_client):
|
||
|
|
"""AC7: POST /api/v1/mcp-client/servers/{invalid_id}/execute → 400 (invalid UUID) or 404."""
|
||
|
|
client, _ = mcp_authed_client
|
||
|
|
resp = await client.post(
|
||
|
|
"/api/v1/mcp-client/servers/00000000-0000-0000-0000-000000000000/execute",
|
||
|
|
json={"tool_name": "test", "arguments": {}},
|
||
|
|
headers=ORIGIN_HEADER,
|
||
|
|
)
|
||
|
|
assert resp.status_code == 404
|
||
|
|
|
||
|
|
|
||
|
|
# ─── AC8: Tool registry integration registers external tools ───
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_ac8_tool_registry_integration(mcp_authed_client):
|
||
|
|
"""AC8: MCP Client plugin registers tools in the AI tool registry."""
|
||
|
|
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
|
||
|
|
from app.plugins.builtins.mcp_client.tool_registry_integration import unregister_all_external_tools, PLUGIN_NAME
|
||
|
|
|
||
|
|
registry = get_tool_registry()
|
||
|
|
# Ensure clean state
|
||
|
|
unregister_all_external_tools()
|
||
|
|
|
||
|
|
# Verify no MCP tools initially
|
||
|
|
mcp_tools = [t for t in registry.get_all() if t.plugin_name == PLUGIN_NAME]
|
||
|
|
assert len(mcp_tools) == 0
|
||
|
|
|
||
|
|
# The integration module exists and has correct functions
|
||
|
|
from app.plugins.builtins.mcp_client.tool_registry_integration import sync_external_tools, _make_tool_name
|
||
|
|
assert callable(sync_external_tools)
|
||
|
|
assert _make_tool_name("My Server", "search") == "mcp__my_server__search"
|