2026-04-30 14:03:16 +01:00
|
|
|
//! MCP HTTP endpoint and tool dispatch module.
|
2026-04-29 21:35:55 +00:00
|
|
|
//!
|
2026-04-30 14:03:16 +01:00
|
|
|
//! Local Claude Code agents connect to `POST /mcp` with a JSON-RPC 2.0
|
|
|
|
|
//! envelope. Tool dispatch is also invoked directly by API-based runtimes
|
|
|
|
|
//! (Gemini, OpenAI) via [`dispatch::dispatch_tool_call`].
|
2026-03-22 19:07:07 +00:00
|
|
|
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for agent start, stop, wait, list, and inspect.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod agent_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for server logs, CRDT dump, version, and story movement.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod diagnostics;
|
2026-04-29 21:35:55 +00:00
|
|
|
/// MCP tool dispatch — routes a tool name to the appropriate handler module.
|
|
|
|
|
pub mod dispatch;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for git operations scoped to agent worktrees.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod git_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for merge status and merge-to-master operations.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod merge_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for QA request, approve, and reject workflows.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod qa_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for running shell commands and test suites.
|
2026-03-22 19:07:07 +00:00
|
|
|
pub mod shell_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for pipeline status, story todos, and triage dump.
|
2026-03-24 11:06:43 +00:00
|
|
|
pub mod status_tools;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for creating, updating, and managing stories and bugs.
|
2026-03-28 14:21:13 +00:00
|
|
|
pub mod story_tools;
|
2026-04-29 21:35:55 +00:00
|
|
|
/// MCP tool schema definitions for `tools/list`.
|
|
|
|
|
pub mod tools_list;
|
2026-04-29 10:41:32 +00:00
|
|
|
/// MCP tools for the project setup wizard.
|
2026-03-28 14:21:13 +00:00
|
|
|
pub mod wizard_tools;
|
2026-03-22 19:07:07 +00:00
|
|
|
|
2026-04-29 21:35:55 +00:00
|
|
|
// Re-export for test code in submodules that references `super::super::handle_tools_list`.
|
2026-03-22 19:07:07 +00:00
|
|
|
#[cfg(test)]
|
2026-04-29 21:35:55 +00:00
|
|
|
pub(crate) use tools_list::handle_tools_list;
|
2026-04-30 14:03:16 +01:00
|
|
|
|
|
|
|
|
use crate::http::context::AppContext;
|
|
|
|
|
use crate::http::gateway::jsonrpc::JsonRpcResponse;
|
|
|
|
|
use poem::handler;
|
|
|
|
|
use poem::http::StatusCode;
|
|
|
|
|
use poem::web::Data;
|
|
|
|
|
use poem::{Body, Request, Response};
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct JsonRpcRequest {
|
|
|
|
|
jsonrpc: String,
|
|
|
|
|
id: Option<Value>,
|
|
|
|
|
method: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
params: Value,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `GET /mcp` — method not allowed (MCP uses POST).
|
|
|
|
|
#[handler]
|
|
|
|
|
pub async fn mcp_get_handler() -> Response {
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::METHOD_NOT_ALLOWED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `POST /mcp` — JSON-RPC 2.0 entry point for `initialize`, `tools/list`,
|
|
|
|
|
/// `tools/call`, and `notifications/*`.
|
|
|
|
|
#[handler]
|
2026-04-30 22:15:37 +00:00
|
|
|
pub async fn mcp_post_handler(req: &Request, body: Body, ctx: Data<&Arc<AppContext>>) -> Response {
|
2026-04-30 14:03:16 +01:00
|
|
|
let content_type = req.header("content-type").unwrap_or("");
|
|
|
|
|
if !content_type.is_empty() && !content_type.contains("application/json") {
|
|
|
|
|
return json_response(JsonRpcResponse::error(
|
|
|
|
|
None,
|
|
|
|
|
-32700,
|
|
|
|
|
"Unsupported Content-Type; expected application/json".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let bytes = match body.into_bytes().await {
|
|
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
return json_response(JsonRpcResponse::error(None, -32700, "Parse error".into()));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rpc: JsonRpcRequest = match serde_json::from_slice(&bytes) {
|
|
|
|
|
Ok(r) => r,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
return json_response(JsonRpcResponse::error(None, -32700, "Parse error".into()));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if rpc.jsonrpc != "2.0" {
|
|
|
|
|
return json_response(JsonRpcResponse::error(
|
|
|
|
|
rpc.id,
|
|
|
|
|
-32600,
|
|
|
|
|
"Invalid JSON-RPC version".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rpc.id.is_none() || rpc.id.as_ref() == Some(&Value::Null) {
|
|
|
|
|
if rpc.method.starts_with("notifications/") {
|
|
|
|
|
return Response::builder()
|
|
|
|
|
.status(StatusCode::ACCEPTED)
|
|
|
|
|
.body(Body::empty());
|
|
|
|
|
}
|
|
|
|
|
return json_response(JsonRpcResponse::error(None, -32600, "Missing id".into()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let resp = match rpc.method.as_str() {
|
|
|
|
|
"initialize" => handle_initialize(rpc.id),
|
|
|
|
|
"tools/list" => {
|
|
|
|
|
JsonRpcResponse::success(rpc.id, json!({ "tools": tools_list::list_tools() }))
|
|
|
|
|
}
|
|
|
|
|
"tools/call" => handle_tools_call(rpc.id, &rpc.params, &ctx).await,
|
|
|
|
|
_ => JsonRpcResponse::error(rpc.id, -32601, format!("Unknown method: {}", rpc.method)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
json_response(resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn json_response(resp: JsonRpcResponse) -> Response {
|
|
|
|
|
let body = serde_json::to_vec(&resp).unwrap_or_default();
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.body(Body::from(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle_initialize(id: Option<Value>) -> JsonRpcResponse {
|
|
|
|
|
JsonRpcResponse::success(
|
|
|
|
|
id,
|
|
|
|
|
json!({
|
|
|
|
|
"protocolVersion": "2025-03-26",
|
|
|
|
|
"capabilities": { "tools": {} },
|
|
|
|
|
"serverInfo": { "name": "huskies", "version": "1.0.0" }
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_tools_call(id: Option<Value>, params: &Value, ctx: &AppContext) -> JsonRpcResponse {
|
|
|
|
|
let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
|
|
|
|
|
|
|
|
|
match dispatch::dispatch_tool_call(tool_name, args, ctx).await {
|
|
|
|
|
Ok(content) => JsonRpcResponse::success(
|
|
|
|
|
id,
|
|
|
|
|
json!({ "content": [{ "type": "text", "text": content }] }),
|
|
|
|
|
),
|
|
|
|
|
Err(msg) => {
|
|
|
|
|
crate::slog_warn!("[mcp] Tool call failed: tool={tool_name} error={msg}");
|
|
|
|
|
JsonRpcResponse::success(
|
|
|
|
|
id,
|
|
|
|
|
json!({
|
|
|
|
|
"content": [{ "type": "text", "text": msg }],
|
|
|
|
|
"isError": true
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|