diff --git a/frontend/src/api/gateway.ts b/frontend/src/api/gateway.ts index 522687bd..c114a0a2 100644 --- a/frontend/src/api/gateway.ts +++ b/frontend/src/api/gateway.ts @@ -73,6 +73,39 @@ async function gatewayRequest( return res.json() as Promise; } +let _mcpRequestId = 1; + +/// Call a gateway MCP tool via JSON-RPC and return the result. +async function gatewayMcpCall( + toolName: string, + args: Record = {}, +): Promise { + const id = _mcpRequestId++; + const body = JSON.stringify({ + jsonrpc: "2.0", + id, + method: "tools/call", + params: { name: toolName, arguments: args }, + }); + const res = await fetch("/mcp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `MCP request failed (${res.status})`); + } + const json = (await res.json()) as { + result?: Record; + error?: { message: string }; + }; + if (json.error) { + throw new Error(json.error.message); + } + return json.result as T; +} + export const gatewayApi = { /// Returns `{ mode: "gateway" }` if this server is a gateway, otherwise rejects. getServerMode(): Promise { @@ -88,7 +121,9 @@ export const gatewayApi = { /// List all build agents that have registered with this gateway. listAgents(): Promise { - return gatewayRequest("/gateway/agents"); + return gatewayMcpCall<{ agents: JoinedAgent[] }>("agents.list").then( + (result) => result.agents ?? [], + ); }, /// Remove a registered build agent by its ID. diff --git a/server/src/gateway.rs b/server/src/gateway.rs index eaefba24..17f4ef98 100644 --- a/server/src/gateway.rs +++ b/server/src/gateway.rs @@ -55,7 +55,6 @@ pub fn build_gateway_route(state_arc: Arc) -> impl poem::Endpoint // Agent registration via CRDT-sync WebSocket. .at("/crdt-sync", poem::get(gateway_crdt_sync_handler)) // Agent management REST endpoints. - .at("/gateway/agents", poem::get(gateway_list_agents_handler)) .at( "/gateway/agents/:id/assign", poem::post(gateway_assign_agent_handler), diff --git a/server/src/http/gateway/mcp.rs b/server/src/http/gateway/mcp.rs index 5f6d92e2..3a5d9dc9 100644 --- a/server/src/http/gateway/mcp.rs +++ b/server/src/http/gateway/mcp.rs @@ -19,6 +19,7 @@ const GATEWAY_TOOLS: &[&str] = &[ "gateway_health", "init_project", "aggregate_pipeline_status", + "agents.list", ]; /// Gateway tool definitions. @@ -84,6 +85,14 @@ pub(crate) fn gateway_tool_definitions() -> Vec { "properties": {} } }), + json!({ + "name": "agents.list", + "description": "List all alive build agents currently registered with this gateway. Returns an array of agent objects with id, label, address, registered_at, last_seen, and assigned_project fields.", + "inputSchema": { + "type": "object", + "properties": {} + } + }), ] } @@ -245,6 +254,7 @@ async fn handle_gateway_tool( "gateway_health" => handle_gateway_health_tool(state, id).await, "init_project" => handle_init_project_tool(params, state, id).await, "aggregate_pipeline_status" => handle_aggregate_pipeline_status_tool(state, id).await, + "agents.list" => handle_agents_list_tool(id), _ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")), } } @@ -426,6 +436,22 @@ async fn handle_aggregate_pipeline_status_tool( ) } +/// Handle the `agents.list` gateway tool — returns all alive build agents from the CRDT. +fn handle_agents_list_tool(id: Option) -> JsonRpcResponse { + let agents = gateway::list_agents(); + let agents_json = serde_json::to_value(&agents).unwrap_or(json!([])); + JsonRpcResponse::success( + id, + json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&agents).unwrap_or_default() + }], + "agents": agents_json, + }), + ) +} + /// Handle the `pipeline.get` read-RPC — returns the same shape as the old /// `GET /api/gateway/pipeline` endpoint: `{ "active": "...", "projects": {...} }`. async fn handle_pipeline_get(state: &GatewayState, id: Option) -> JsonRpcResponse { diff --git a/server/src/http/gateway/mod.rs b/server/src/http/gateway/mod.rs index 64bfe4f4..303b8d67 100644 --- a/server/src/http/gateway/mod.rs +++ b/server/src/http/gateway/mod.rs @@ -18,7 +18,7 @@ pub use mcp::{gateway_mcp_get_handler, gateway_mcp_post_handler}; pub use rest::{ gateway_add_project_handler, gateway_api_handler, gateway_assign_agent_handler, gateway_bot_config_get_handler, gateway_bot_config_page_handler, - gateway_bot_config_save_handler, gateway_generate_token_handler, gateway_list_agents_handler, - gateway_mode_handler, gateway_remove_project_handler, + gateway_bot_config_save_handler, gateway_generate_token_handler, gateway_mode_handler, + gateway_remove_project_handler, }; pub use websocket::{gateway_crdt_sync_handler, gateway_event_push_handler}; diff --git a/server/src/http/gateway/rest.rs b/server/src/http/gateway/rest.rs index 730762b9..2cb75fb6 100644 --- a/server/src/http/gateway/rest.rs +++ b/server/src/http/gateway/rest.rs @@ -33,17 +33,6 @@ pub async fn gateway_generate_token_handler(state: Data<&Arc>) -> .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } -/// `GET /gateway/agents` — list all alive build agents registered in the CRDT. -#[handler] -pub async fn gateway_list_agents_handler(_state: Data<&Arc>) -> Response { - let agents = gateway::list_agents(); - let body = serde_json::to_vec(&agents).unwrap_or_default(); - Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "application/json") - .body(Body::from(body)) -} - /// Request body for assigning an agent to a project. #[derive(Deserialize)] struct AssignAgentRequest {