825 lines
26 KiB
Python
825 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Integration tests for MCP client APIs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p0
|
|
def test_mcp_create_get_list_delete(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify MCP client basic lifecycle in one agent workspace.
|
|
|
|
Test flow:
|
|
1. Create a test agent.
|
|
2. POST /api/mcp to create one MCP client.
|
|
3. GET /api/mcp and GET /api/mcp/{client_key} to verify presence.
|
|
4. DELETE /api/mcp/{client_key}.
|
|
5. GET /api/mcp and verify client is removed.
|
|
6. Delete test agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- GET /api/mcp
|
|
- GET /api/mcp/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_crud_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_client_01"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP CRUD agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "integration mcp client",
|
|
"description": "created by integration tests",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["mcp"],
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
created = create_client.json()
|
|
assert created.get("key") == client_key
|
|
assert created.get("name") == "integration mcp client"
|
|
|
|
list_clients = app_server.api_request(
|
|
"GET",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
)
|
|
assert list_clients.status_code == 200, app_server.logs_tail()
|
|
keys = {item["key"] for item in list_clients.json()}
|
|
assert client_key in keys
|
|
|
|
get_client = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert get_client.status_code == 200, app_server.logs_tail()
|
|
assert get_client.json().get("key") == client_key
|
|
|
|
delete_client = app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert delete_client.status_code == 200, app_server.logs_tail()
|
|
|
|
deadline = time.time() + 3.0
|
|
while time.time() < deadline:
|
|
list_after_delete = app_server.api_request(
|
|
"GET",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
)
|
|
assert list_after_delete.status_code == 200, app_server.logs_tail()
|
|
keys_after = {item["key"] for item in list_after_delete.json()}
|
|
if client_key not in keys_after:
|
|
break
|
|
time.sleep(0.3)
|
|
else:
|
|
keys_after = {item["key"] for item in list_after_delete.json()}
|
|
assert client_key not in keys_after, (
|
|
f"MCP client {client_key!r} still in list after 3s: "
|
|
f"{keys_after}\n{app_server.logs_tail()}"
|
|
)
|
|
finally:
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p0
|
|
def test_mcp_toggle_enabled(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify MCP toggle endpoint flips ``enabled`` state and persists.
|
|
|
|
Test flow:
|
|
1. Create a test agent and one MCP client with enabled=True.
|
|
2. PATCH /api/mcp/toggle/{client_key} and assert enabled=False.
|
|
3. GET /api/mcp/{client_key} and verify enabled=False persisted.
|
|
4. PATCH again and verify enabled=True.
|
|
5. Cleanup client and agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- PATCH /api/mcp/toggle/{client_key}
|
|
- GET /api/mcp/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_toggle_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_toggle_client"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP toggle agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "toggle mcp client",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
assert create_client.json().get("enabled") is True
|
|
|
|
toggle_1 = app_server.api_request(
|
|
"PATCH",
|
|
f"/api/mcp/toggle/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert toggle_1.status_code == 200, app_server.logs_tail()
|
|
assert toggle_1.json().get("enabled") is False
|
|
|
|
get_after_toggle_1 = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert get_after_toggle_1.status_code == 200, app_server.logs_tail()
|
|
assert get_after_toggle_1.json().get("enabled") is False
|
|
|
|
toggle_2 = app_server.api_request(
|
|
"PATCH",
|
|
f"/api/mcp/toggle/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert toggle_2.status_code == 200, app_server.logs_tail()
|
|
assert toggle_2.json().get("enabled") is True
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p2
|
|
def test_mcp_tools_returns_empty_for_disabled_client(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify /tools endpoint returns empty list when MCP client is disabled.
|
|
|
|
Test flow:
|
|
1. Create a test agent.
|
|
2. Create a disabled MCP client.
|
|
3. GET /api/mcp/tools/{client_key} and assert empty list.
|
|
4. Cleanup client and agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- GET /api/mcp/tools/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_tools_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_disabled_tools_client"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP tools agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "disabled mcp client",
|
|
"enabled": False,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
|
|
tools_resp = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/tools/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert tools_resp.status_code == 200, app_server.logs_tail()
|
|
assert tools_resp.json() == []
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
def test_mcp_create_writes_driver_card_and_credentials_not_agent_json(
|
|
app_server,
|
|
) -> None:
|
|
agent_id = "integ_mcp_driver_storage_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_driver_storage_client"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP Driver storage", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
workspace_dir = Path(create_agent.json()["workspace_dir"])
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "driver storage mcp",
|
|
"enabled": False,
|
|
"transport": "stdio",
|
|
"command": "python",
|
|
"env": {"ECHO_SECRET": "secret-value"},
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
|
|
card_path = workspace_dir / "drivers" / "mcp" / f"{client_key}.yaml"
|
|
flat_card_path = workspace_dir / "drivers" / f"{client_key}.yaml"
|
|
credentials_path = workspace_dir / "credentials.yaml"
|
|
agent_config = json.loads(
|
|
(workspace_dir / "agent.json").read_text(encoding="utf-8"),
|
|
)
|
|
card = yaml.safe_load(card_path.read_text(encoding="utf-8"))
|
|
credentials = yaml.safe_load(
|
|
credentials_path.read_text(encoding="utf-8"),
|
|
)
|
|
|
|
assert not flat_card_path.exists()
|
|
assert card["protocol"] == "mcp"
|
|
assert card["credentials"]["static"] == {
|
|
"kind": "static",
|
|
"ref": f"mcp/{client_key}",
|
|
}
|
|
assert card["endpoint"]["env"]["ECHO_SECRET"] == {
|
|
"source": "credential",
|
|
"credential": "static",
|
|
"field": "ECHO_SECRET",
|
|
}
|
|
assert "secret-value" not in card_path.read_text(encoding="utf-8")
|
|
assert credentials["credentials"][f"mcp/{client_key}"]["secrets"][
|
|
"ECHO_SECRET"
|
|
].startswith("ENC:")
|
|
old_clients = (agent_config.get("mcp") or {}).get("clients") or {}
|
|
assert client_key not in old_clients
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
def test_mcp_scoped_path_get_client(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify MCP client can be fetched via ``/api/agents/{agentId}/mcp/...``
|
|
without ``X-Agent-Id`` header (path-derived agent context).
|
|
|
|
Test flow:
|
|
1. Create a test agent.
|
|
2. POST /api/mcp with header to create a client (same as existing tests).
|
|
3. GET /api/agents/{agentId}/mcp/{client_key} without header.
|
|
4. Cleanup via header-based DELETE and agent delete.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- GET /api/agents/{agentId}/mcp/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_scoped_get_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_scoped_get_client"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={
|
|
"id": agent_id,
|
|
"name": "MCP scoped GET agent",
|
|
"description": "",
|
|
},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "scoped path get client",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["scoped"],
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
|
|
scoped_get = app_server.api_request(
|
|
"GET",
|
|
f"/api/agents/{agent_id}/mcp/{client_key}",
|
|
)
|
|
assert scoped_get.status_code == 200, app_server.logs_tail()
|
|
assert scoped_get.json().get("key") == client_key
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p2
|
|
def test_mcp_create_duplicate_client_rejected(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify creating an MCP client with duplicated ``client_key`` is rejected.
|
|
|
|
Test flow:
|
|
1. Create a dedicated test agent.
|
|
2. POST /api/mcp once with a fixed client_key (expect 201).
|
|
3. POST /api/mcp again with the same client_key (expect 400).
|
|
4. Assert error detail includes guidance about existing client.
|
|
5. Cleanup client and agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_duplicate_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_dup_client"
|
|
payload = {
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "duplicate check client",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["dup"],
|
|
},
|
|
}
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={
|
|
"id": agent_id,
|
|
"name": "MCP duplicate agent",
|
|
"description": "",
|
|
},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
first = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json=payload,
|
|
)
|
|
assert first.status_code == 201, app_server.logs_tail()
|
|
|
|
second = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json=payload,
|
|
)
|
|
assert second.status_code == 400, app_server.logs_tail()
|
|
assert "already exists" in second.json().get("detail", "")
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
def test_mcp_create_or_update_duplicate_display_name_rejected(
|
|
app_server,
|
|
) -> None:
|
|
"""Verify MCP display names are unique user-facing identifiers."""
|
|
agent_id = "integ_mcp_duplicate_name_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
first_key = "integ_mcp_dup_name_first"
|
|
second_key = "integ_mcp_dup_name_second"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={
|
|
"id": agent_id,
|
|
"name": "MCP duplicate name agent",
|
|
"description": "",
|
|
},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
first = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": first_key,
|
|
"client": {
|
|
"name": "aone-code-platform",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["first"],
|
|
},
|
|
},
|
|
)
|
|
assert first.status_code == 201, app_server.logs_tail()
|
|
|
|
duplicate_create = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": second_key,
|
|
"client": {
|
|
"name": "AONE-CODE-PLATFORM",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["second"],
|
|
},
|
|
},
|
|
)
|
|
assert duplicate_create.status_code == 400, app_server.logs_tail()
|
|
assert "already exists" in duplicate_create.json().get("detail", "")
|
|
|
|
second = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": second_key,
|
|
"client": {
|
|
"name": "other-platform",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["second"],
|
|
},
|
|
},
|
|
)
|
|
assert second.status_code == 201, app_server.logs_tail()
|
|
|
|
duplicate_update = app_server.api_request(
|
|
"PUT",
|
|
f"/api/mcp/{second_key}",
|
|
headers=headers,
|
|
json={"name": "aone-code-platform"},
|
|
)
|
|
assert duplicate_update.status_code == 400, app_server.logs_tail()
|
|
assert "already exists" in duplicate_update.json().get("detail", "")
|
|
finally:
|
|
for key in (first_key, second_key):
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p2
|
|
def test_mcp_missing_client_paths_return_404(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify MCP endpoints consistently return 404 for unknown ``client_key``.
|
|
|
|
Test flow:
|
|
1. Create a dedicated test agent.
|
|
2. Call GET /api/mcp/{client_key}, PATCH /toggle/{client_key}, DELETE,
|
|
and GET /tools/{client_key} using a missing client_key.
|
|
3. Assert all responses are 404 with error details.
|
|
4. Delete test agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- GET /api/mcp/{client_key}
|
|
- PATCH /api/mcp/toggle/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- GET /api/mcp/tools/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_missing_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
missing_key = "integ_mcp_missing_key"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP missing agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
get_resp = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/{missing_key}",
|
|
headers=headers,
|
|
)
|
|
assert get_resp.status_code == 404, app_server.logs_tail()
|
|
assert "detail" in get_resp.json()
|
|
|
|
toggle_resp = app_server.api_request(
|
|
"PATCH",
|
|
f"/api/mcp/toggle/{missing_key}",
|
|
headers=headers,
|
|
)
|
|
assert toggle_resp.status_code == 404, app_server.logs_tail()
|
|
assert "detail" in toggle_resp.json()
|
|
|
|
delete_resp = app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{missing_key}",
|
|
headers=headers,
|
|
)
|
|
assert delete_resp.status_code == 404, app_server.logs_tail()
|
|
assert "detail" in delete_resp.json()
|
|
|
|
tools_resp = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/tools/{missing_key}",
|
|
headers=headers,
|
|
)
|
|
assert tools_resp.status_code == 404, app_server.logs_tail()
|
|
assert "detail" in tools_resp.json()
|
|
finally:
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
def test_mcp_update_client_put_roundtrip(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify MCP client update endpoint persists mutable fields and supports
|
|
deterministic readback.
|
|
|
|
Test flow:
|
|
1. Create a dedicated test agent and one MCP client.
|
|
2. PUT /api/mcp/{client_key} with updated name/description/enabled fields.
|
|
3. GET /api/mcp/{client_key} and verify updated fields.
|
|
4. Cleanup client and agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/mcp
|
|
- PUT /api/mcp/{client_key}
|
|
- GET /api/mcp/{client_key}
|
|
- DELETE /api/mcp/{client_key}
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_mcp_update_01"
|
|
headers = {"X-Agent-Id": agent_id}
|
|
client_key = "integ_mcp_update_client"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "MCP update agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
"/api/mcp",
|
|
headers=headers,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "update before",
|
|
"description": "before update",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
"args": ["before"],
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
|
|
update_resp = app_server.api_request(
|
|
"PUT",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
json={
|
|
"name": "update after",
|
|
"description": "after update",
|
|
"enabled": False,
|
|
"args": ["after"],
|
|
},
|
|
)
|
|
assert update_resp.status_code == 200, app_server.logs_tail()
|
|
update_payload = update_resp.json()
|
|
assert update_payload.get("key") == client_key
|
|
assert update_payload.get("name") == "update after"
|
|
assert update_payload.get("description") == "after update"
|
|
assert update_payload.get("enabled") is False
|
|
|
|
get_after = app_server.api_request(
|
|
"GET",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
assert get_after.status_code == 200, app_server.logs_tail()
|
|
get_payload = get_after.json()
|
|
assert get_payload.get("name") == "update after"
|
|
assert get_payload.get("description") == "after update"
|
|
assert get_payload.get("enabled") is False
|
|
finally:
|
|
app_server.api_request(
|
|
"DELETE",
|
|
f"/api/mcp/{client_key}",
|
|
headers=headers,
|
|
)
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
def test_mcp_agent_scoped_routes_update_toggle_delete(app_server) -> None:
|
|
"""Test purpose:
|
|
- Verify agent-scoped MCP routes support create/update/toggle/delete flow
|
|
under /api/agents/{agentId}/mcp.
|
|
|
|
Test flow:
|
|
1. Create a dedicated test agent.
|
|
2. POST scoped MCP client.
|
|
3. PUT scoped client and verify updated fields.
|
|
4. PATCH scoped toggle and verify enabled flips.
|
|
5. DELETE scoped client and verify it is removed from scoped list.
|
|
6. Delete test agent.
|
|
|
|
API endpoints:
|
|
- POST /api/agents
|
|
- POST /api/agents/{agentId}/mcp
|
|
- PUT /api/agents/{agentId}/mcp/{client_key}
|
|
- PATCH /api/agents/{agentId}/mcp/toggle/{client_key}
|
|
- DELETE /api/agents/{agentId}/mcp/{client_key}
|
|
- GET /api/agents/{agentId}/mcp
|
|
- DELETE /api/agents/{agentId}
|
|
"""
|
|
agent_id = "integ_scoped_mcp_01"
|
|
client_key = "integ_scoped_mcp_client_01"
|
|
scoped_base = f"/api/agents/{agent_id}/mcp"
|
|
|
|
create_agent = app_server.api_request(
|
|
"POST",
|
|
"/api/agents",
|
|
json={"id": agent_id, "name": "Scoped MCP agent", "description": ""},
|
|
)
|
|
assert create_agent.status_code == 201, app_server.logs_tail()
|
|
|
|
try:
|
|
create_client = app_server.api_request(
|
|
"POST",
|
|
scoped_base,
|
|
json={
|
|
"client_key": client_key,
|
|
"client": {
|
|
"name": "scoped mcp before",
|
|
"description": "before update",
|
|
"enabled": True,
|
|
"transport": "stdio",
|
|
"command": "echo",
|
|
},
|
|
},
|
|
)
|
|
assert create_client.status_code == 201, app_server.logs_tail()
|
|
assert create_client.json().get("key") == client_key
|
|
|
|
update_client = app_server.api_request(
|
|
"PUT",
|
|
f"{scoped_base}/{client_key}",
|
|
json={
|
|
"name": "scoped mcp after",
|
|
"description": "after update",
|
|
"enabled": False,
|
|
"args": ["scoped"],
|
|
},
|
|
)
|
|
assert update_client.status_code == 200, app_server.logs_tail()
|
|
assert update_client.json().get("name") == "scoped mcp after"
|
|
assert update_client.json().get("enabled") is False
|
|
|
|
# On Windows, PUT triggers a fire-and-forget
|
|
# reload_driver_best_effort that opens the yaml file in a
|
|
# background task. If PATCH toggle fires before that task
|
|
# finishes, os.replace in dump_card hits WinError 5 (target
|
|
# held open). POSIX allows rename-over-open, so this only
|
|
# affects Windows.
|
|
if sys.platform == "win32":
|
|
time.sleep(1.0)
|
|
|
|
toggle_client = app_server.api_request(
|
|
"PATCH",
|
|
f"{scoped_base}/toggle/{client_key}",
|
|
)
|
|
assert toggle_client.status_code == 200, app_server.logs_tail()
|
|
assert toggle_client.json().get("enabled") is True
|
|
|
|
delete_client = app_server.api_request(
|
|
"DELETE",
|
|
f"{scoped_base}/{client_key}",
|
|
)
|
|
assert delete_client.status_code == 200, app_server.logs_tail()
|
|
|
|
deadline = time.time() + 3.0
|
|
while time.time() < deadline:
|
|
list_after = app_server.api_request("GET", scoped_base)
|
|
assert list_after.status_code == 200, app_server.logs_tail()
|
|
keys_after = {item["key"] for item in list_after.json()}
|
|
if client_key not in keys_after:
|
|
break
|
|
time.sleep(0.3)
|
|
else:
|
|
keys_after = {item["key"] for item in list_after.json()}
|
|
assert client_key not in keys_after, (
|
|
f"MCP client {client_key!r} still in list after 3s: "
|
|
f"{keys_after}\n{app_server.logs_tail()}"
|
|
)
|
|
finally:
|
|
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
|