use super::state::AppState;
use axum::{
Json, Router,
extract::State,
http::{HeaderMap, StatusCode, header},
routing::{get, post},
};
use serde_json::{Value, json};
use tower_http::cors::{Any, CorsLayer};
use tracing::{debug, warn};
pub fn build_router(state: AppState) -> Router {
let cors = CorsLayer::new()
.allow_origin(Any) .allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::OPTIONS,
])
.allow_headers([
header::CONTENT_TYPE,
header::AUTHORIZATION,
header::HeaderName::from_static("x-request-id"),
]);
Router::new()
.route("/health", get(health_handler))
.route("/mcp", post(mcp_dispatch_handler))
.route("/mcp/tools/list", get(tools_list_handler))
.route("/mcp/tools/call", post(tools_call_handler))
.layer(cors)
.with_state(state)
}
async fn health_handler() -> Json<Value> {
Json(json!({
"status": "ok",
"service": "stygian-plugin-mcp"
}))
}
async fn mcp_dispatch_handler(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> (StatusCode, Json<Value>) {
let req_id = extract_request_id(&headers);
debug!(
req_id,
method = body.get("method").and_then(|v| v.as_str()),
"MCP dispatch"
);
state
.handler
.handle(&body)
.await
.map_or((StatusCode::NO_CONTENT, Json(Value::Null)), |response| {
(StatusCode::OK, Json(response))
})
}
async fn tools_list_handler(State(state): State<AppState>) -> Json<Value> {
let req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
});
state.handler.handle(&req).await.map_or_else(
|| Json(json!({ "jsonrpc": "2.0", "id": 1, "result": { "tools": [] } })),
Json,
)
}
async fn tools_call_handler(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> (StatusCode, Json<Value>) {
let req_id = extract_request_id(&headers);
let request = if body.get("jsonrpc").is_some() {
body
} else {
debug!(req_id, "bare tool call — wrapping in JSON-RPC envelope");
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": body
})
};
let tool_name = request
.get("params")
.and_then(|p| p.get("name"))
.and_then(Value::as_str);
if tool_name.is_none() {
warn!(req_id, "tool call missing 'params.name'");
return (
StatusCode::BAD_REQUEST,
Json(json!({
"jsonrpc": "2.0",
"id": request.get("id").cloned().unwrap_or(Value::Null),
"error": {
"code": -32602,
"message": "Invalid params: missing 'name' in params"
}
})),
);
}
debug!(req_id, tool = tool_name, "tool call");
state
.handler
.handle(&request)
.await
.map_or((StatusCode::NO_CONTENT, Json(Value::Null)), |response| {
(StatusCode::OK, Json(response))
})
}
fn extract_request_id(headers: &HeaderMap) -> &str {
headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("-")
}