feat(host-agent): mount /mcp + surface MCP status in console

This commit is contained in:
2026-07-21 15:09:41 +08:00
parent dcb4798408
commit ce2469616e
3 changed files with 192 additions and 1 deletions
@@ -6,6 +6,7 @@ from datetime import UTC, datetime
from typing import Any
from fastapi.testclient import TestClient
from mcp.server.fastmcp import FastMCP
from cloud.internal_api.models import AssignmentModel
from core.models import Task
@@ -15,9 +16,12 @@ from host_agent.config import HostAgentConfig
from host_agent.history import ConsoleHistoryStore
from host_agent.identity import HostIdentityStore
from host_agent.local_account import LocalAccountStore
from host_agent.mcp_lock import McpBusyTracker
from host_agent.mcp_token import McpTokenStore
from host_agent.status import AgentStatusTracker
from host_agent.web.app import SESSION_COOKIE_NAME, create_console_app
from host_agent.web.auth import SessionManager
from host_agent.web.mcp import build_mcp_server
from storage.device_config import DeviceConfigStore
from storage.task_metadata import TaskMetadataStore
@@ -34,6 +38,9 @@ def _build_client(
submit_self_task: TaskSubmissionCallable | None = None,
cancel_task: TaskCancellationCallable | None = None,
include_metadata_store: bool = True,
mcp_server: FastMCP | None = None,
mcp_token_store: McpTokenStore | None = None,
mcp_busy_tracker: McpBusyTracker | None = None,
) -> tuple[TestClient, dict]:
config = HostAgentConfig(
control_plane_url="https://control.example",
@@ -68,6 +75,9 @@ def _build_client(
submit_self_task=submit_self_task,
cancel_task=cancel_task,
metadata_store=metadata_store,
mcp_server=mcp_server,
mcp_token_store=mcp_token_store,
mcp_busy_tracker=mcp_busy_tracker,
)
client = TestClient(app)
context = {
@@ -959,3 +969,131 @@ def test_cancel_task_without_csrf_token_is_rejected(tmp_path) -> None:
assert response.status_code == 403
assert captured == {}
# ---------------------------------------------------------------------------
# MCP mount + /api/status fields + dashboard row
# ---------------------------------------------------------------------------
def _build_mcp_components(tmp_path) -> tuple[FastMCP, McpTokenStore, McpBusyTracker]:
manager = DeviceManager()
status_tracker = AgentStatusTracker()
tracker = McpBusyTracker()
token_store = McpTokenStore(tmp_path / "host_mcp_token.json")
server = build_mcp_server(
manager=manager,
mcp_busy_tracker=tracker,
status_tracker=status_tracker,
)
return server, token_store, tracker
def test_console_app_mounts_mcp_when_all_components_provided(tmp_path) -> None:
server, token_store, tracker = _build_mcp_components(tmp_path)
client, _ = _build_client(
tmp_path,
mcp_server=server,
mcp_token_store=token_store,
mcp_busy_tracker=tracker,
)
# Without auth, the bearer middleware should respond 401 — not 404.
resp = client.post("/mcp/", json={"jsonrpc": "2.0", "method": "ping", "id": 1})
assert resp.status_code != 404
def test_console_app_does_not_mount_mcp_when_components_missing(tmp_path) -> None:
client, _ = _build_client(tmp_path)
resp = client.post("/mcp/", json={"jsonrpc": "2.0", "method": "ping", "id": 1})
assert resp.status_code == 404
def test_api_status_includes_mcp_busy_devices(tmp_path) -> None:
server, token_store, tracker = _build_mcp_components(tmp_path)
# Acquire a lease without going through HTTP — tracker exposes a direct API.
tracker.acquire("phone-1", "test-session")
client, _ = _build_client(
tmp_path,
mcp_server=server,
mcp_token_store=token_store,
mcp_busy_tracker=tracker,
)
_login(client)
response = client.get("/api/status")
assert response.status_code == 200
body = response.json()
assert "mcp_busy_devices" in body
assert "phone-1" in body["mcp_busy_devices"]
assert body["mcp_endpoint"] == "/mcp"
def test_api_status_omits_mcp_fields_when_components_missing(tmp_path) -> None:
client, _ = _build_client(tmp_path)
_login(client)
response = client.get("/api/status")
assert response.status_code == 200
body = response.json()
assert body["mcp_busy_devices"] == []
assert body["mcp_endpoint"] is None
def test_dashboard_renders_mcp_status_row(tmp_path) -> None:
server, token_store, tracker = _build_mcp_components(tmp_path)
tracker.acquire("phone-1", "test-session")
client, _ = _build_client(
tmp_path,
mcp_server=server,
mcp_token_store=token_store,
mcp_busy_tracker=tracker,
)
_login(client)
response = client.get("/")
assert response.status_code == 200
text = response.text
assert "<td>MCP</td>" in text
assert "/mcp" in text
assert "phone-1" in text
def test_dashboard_renders_mcp_not_configured_when_components_missing(
tmp_path,
) -> None:
client, _ = _build_client(tmp_path)
_login(client)
response = client.get("/")
assert response.status_code == 200
text = response.text
assert "<td>MCP</td>" in text
assert "not configured" in text
def test_mcp_endpoint_unauthorized_without_bearer_token(tmp_path) -> None:
server, token_store, tracker = _build_mcp_components(tmp_path)
client, _ = _build_client(
tmp_path,
mcp_server=server,
mcp_token_store=token_store,
mcp_busy_tracker=tracker,
)
resp = client.post("/mcp/", json={"jsonrpc": "2.0", "method": "ping", "id": 1})
assert resp.status_code == 401
def test_mcp_endpoint_rejects_invalid_bearer_token(tmp_path) -> None:
server, token_store, tracker = _build_mcp_components(tmp_path)
client, _ = _build_client(
tmp_path,
mcp_server=server,
mcp_token_store=token_store,
mcp_busy_tracker=tracker,
)
resp = client.post(
"/mcp/",
headers={"Authorization": "Bearer not-the-real-token"},
json={"jsonrpc": "2.0", "method": "ping", "id": 1},
)
assert resp.status_code == 401