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
+36 -1
View File
@@ -24,6 +24,8 @@ from host_agent.devices import register_local_device, unregister_local_device
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.auth import (
SessionManager,
@@ -31,6 +33,7 @@ from host_agent.web.auth import (
attempt_login,
change_password,
)
from host_agent.web.mcp_auth import BearerAuthMiddleware
from storage.device_config import DeviceConfigStore
from storage.task_metadata import TaskMetadataStore
from storage.timeline import Timeline
@@ -223,6 +226,9 @@ def create_console_app(
metadata_store: TaskMetadataStore | None = None,
timeline: Timeline | None = None,
executor: AssignmentExecutor | None = None,
mcp_server: Any = None,
mcp_token_store: McpTokenStore | None = None,
mcp_busy_tracker: McpBusyTracker | None = None,
) -> FastAPI:
app = FastAPI(title="Host Agent Console")
cookie_secure = config.console_bind_host not in _LOOPBACK_BIND_HOSTS
@@ -231,6 +237,18 @@ def create_console_app(
if cancel_task is None and host_client is not None:
cancel_task = host_client.cancel_task
submission_available = submit_self_task is not None
mcp_mounted = mcp_server is not None and mcp_token_store is not None
if mcp_mounted:
from starlette.applications import Starlette
from starlette.middleware import Middleware
mcp_asgi = mcp_server.streamable_http_app()
authed = Starlette(
routes=[],
middleware=[Middleware(BearerAuthMiddleware, token_store=mcp_token_store)],
)
authed.router.mount("/", mcp_asgi)
app.mount("/mcp", authed)
def _running_devices() -> list[dict[str, str]]:
return [
@@ -340,6 +358,10 @@ def create_console_app(
for d in manager.list_devices()
]
texts = _dashboard_texts(snapshot=snapshot)
mcp_endpoint = "/mcp" if mcp_mounted else None
mcp_busy_devices = (
mcp_busy_tracker.busy_device_ids() if mcp_busy_tracker is not None else []
)
return _render(
"dashboard.html",
title="Status",
@@ -347,6 +369,8 @@ def create_console_app(
identity=identity,
devices=devices,
config=config,
mcp_endpoint=mcp_endpoint,
mcp_busy_devices=mcp_busy_devices,
**texts,
)
@@ -375,7 +399,18 @@ def create_console_app(
}
for device in manager.list_devices()
]
return JSONResponse({"status": snapshot, "devices": devices})
return JSONResponse(
{
"status": snapshot,
"devices": devices,
"mcp_endpoint": "/mcp" if mcp_mounted else None,
"mcp_busy_devices": (
mcp_busy_tracker.busy_device_ids()
if mcp_busy_tracker is not None
else []
),
}
)
@app.get("/devices", response_class=HTMLResponse)
async def devices_page(
@@ -20,6 +20,24 @@
<p id="current-assignment">{{ assignment_text }}</p>
<p id="current-progress">{{ progress_text }}</p>
</section>
<section>
<h2>MCP</h2>
<table>
<tbody>
<tr>
<td>MCP</td>
<td>
{% if mcp_endpoint %}
endpoint <code>{{ mcp_endpoint }}</code>;
{% if mcp_busy_devices %}busy: {{ mcp_busy_devices|join(", ") }}{% else %}idle{% endif %}
{% else %}
not configured
{% endif %}
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Devices</h2>
<table>