use super::AppState;
use crate::core::mcp::McpServer;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
pub async fn start_http_server(
bind: String,
port: u16,
api_key: Option<String>,
policy: Option<PathBuf>,
spec: Option<PathBuf>,
allow_localhost: bool,
debug: bool,
) -> anyhow::Result<()> {
if allow_localhost {
std::env::set_var("MRAPIDS_ALLOW_LOCALHOST", "true");
}
let spec_display = spec
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "auto-detect".to_string());
let policy_display = policy
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "permissive".to_string());
let has_api_key = api_key.is_some();
let mut server = McpServer::new(debug, policy, spec);
if let Err(e) = server.initialize() {
eprintln!("Warning: initialization error: {} (continuing)", e);
}
let state = AppState {
server: Arc::new(Mutex::new(server)),
api_key,
start_time: std::time::Instant::now(),
allow_localhost,
};
let app = Router::new()
.route("/health", get(handle_health))
.route("/api/operations", get(handle_list_operations))
.route("/api/find", post(handle_find))
.route("/api/execute", post(handle_execute))
.with_state(state);
let addr: SocketAddr = format!("{}:{}", bind, port).parse()?;
eprintln!("🚀 mrapids HTTP server starting on http://{}", addr);
if has_api_key {
eprintln!("🔒 API key authentication enabled");
} else {
eprintln!("⚠️ No API key configured — accepting all requests");
}
eprintln!(" Spec: {}", spec_display);
eprintln!(" Policy: {}", policy_display);
eprintln!(
" Base URL: {}",
std::env::var("API_BASE_URL").unwrap_or_else(|_| "from spec/config".to_string())
);
eprintln!(
" Localhost: {}",
if allow_localhost {
"allowed"
} else {
"blocked"
}
);
eprintln!();
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
async fn handle_health(State(state): State<AppState>) -> Json<serde_json::Value> {
let uptime = state.start_time.elapsed().as_secs();
Json(serde_json::json!({
"status": "ok",
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": uptime,
}))
}
#[derive(Deserialize)]
struct FindRequest {
query: String,
method: Option<String>,
limit: Option<usize>,
}
async fn handle_list_operations(State(state): State<AppState>) -> impl IntoResponse {
let exe_path = std::env::current_exe().unwrap_or_else(|_| "mrapids".into());
let mut args = vec![
"list".to_string(),
"operations".to_string(),
"--json".to_string(),
];
{
let server = state.server.lock().await;
if let Ok(spec_path) = server.find_spec_file() {
args.insert(2, spec_path.display().to_string()); }
}
match tokio::process::Command::new(&exe_path)
.args(&args)
.output()
.await
{
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<serde_json::Value>(&stdout) {
Ok(parsed) => (StatusCode::OK, Json(parsed)),
Err(_) => {
let stderr = String::from_utf8_lossy(&output.stderr);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "list_failed",
"message": "Could not list operations from spec.",
"hint": if stderr.contains("No API specification") {
"No spec file found. Start server with --spec flag."
} else {
"Check that the spec file exists and is valid OpenAPI."
},
"stderr": stderr.to_string(),
})),
)
}
}
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "subprocess_failed",
"message": format!("Could not run list command: {}", e),
"hint": "Check that mrapids binary is accessible.",
})),
),
}
}
async fn handle_find(
State(state): State<AppState>,
Json(req): Json<FindRequest>,
) -> impl IntoResponse {
let exe_path = std::env::current_exe().unwrap_or_else(|_| "mrapids".into());
let spec_path = {
let server = state.server.lock().await;
server
.find_spec_file()
.ok()
.map(|p| p.display().to_string())
};
let mut args = vec![
"find".to_string(),
req.query.clone(),
"--format".to_string(),
"json".to_string(),
];
if let Some(limit) = req.limit {
args.push("--limit".to_string());
args.push(limit.to_string());
}
if let Ok(output) = tokio::process::Command::new(&exe_path)
.args(&args)
.output()
.await
{
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stdout) {
let has_results = parsed.get("count").and_then(|c| c.as_u64()).unwrap_or(0) > 0
|| parsed
.get("results")
.and_then(|r| r.as_array())
.map(|a| !a.is_empty())
.unwrap_or(false);
if has_results {
return (StatusCode::OK, Json(parsed));
}
}
}
if let Some(ref spec) = spec_path {
let mut explore_args = vec![
"explore".to_string(),
req.query.clone(),
"--format".to_string(),
"json".to_string(),
];
explore_args.push("--spec".to_string());
explore_args.push(spec.clone());
if let Ok(output) = tokio::process::Command::new(&exe_path)
.args(&explore_args)
.output()
.await
{
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stdout) {
let is_empty = parsed.as_array().map(|a| a.is_empty()).unwrap_or(false);
if !is_empty {
return (StatusCode::OK, Json(parsed));
}
}
}
}
(
StatusCode::OK,
Json(serde_json::json!({
"results": [],
"count": 0,
"error": "search_failed",
"message": format!("No operations found matching '{}'. Both index search and spec exploration returned empty.", req.query),
"hint": "Try a different search term, or build the index: mrapids index build --spec <your-spec>",
"possible_causes": [
"Query too specific — try shorter keywords (e.g., 'portfolio' instead of 'get my portfolio data')",
"No index built — run: mrapids index build --spec <your-spec>",
"Spec has no descriptions — keyword search needs operation summaries to match",
],
"spec_loaded": spec_path.is_some(),
})),
)
}
#[derive(Deserialize)]
struct ExecuteRequest {
operation_id: String,
params: Option<serde_json::Value>,
body: Option<serde_json::Value>,
select: Option<Vec<String>>,
max_items: Option<usize>,
}
async fn handle_execute(
State(state): State<AppState>,
Json(req): Json<ExecuteRequest>,
) -> impl IntoResponse {
let exe_path = std::env::current_exe().unwrap_or_else(|_| "mrapids".into());
let mut args = vec![
"run".to_string(),
req.operation_id.clone(),
"--json-output".to_string(),
"--redact".to_string(),
];
if state.allow_localhost {
args.push("--allow-localhost".to_string());
}
{
let server = state.server.lock().await;
if let Ok(spec_path) = server.find_spec_file() {
args.push("--spec".to_string());
args.push(spec_path.display().to_string());
}
}
if let Some(params) = &req.params {
if let Some(obj) = params.as_object() {
for (key, value) in obj {
let value_str = match value {
serde_json::Value::String(s) => s.clone(),
_ => value.to_string(),
};
args.push("--param".to_string());
args.push(format!("{}={}", key, value_str));
}
}
}
if let Some(body) = &req.body {
args.push("--data".to_string());
args.push(body.to_string());
}
let output = match tokio::process::Command::new(&exe_path)
.args(&args)
.output()
.await
{
Ok(output) => output,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "execution_failed",
"message": format!("Failed to execute operation '{}': {}", req.operation_id, e),
"hint": "Check that mrapids binary is accessible and the spec file exists.",
"operation_id": req.operation_id,
})),
);
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<serde_json::Value>(&stdout) {
Ok(response) => {
let success = response
.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let status = if success {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
};
(status, Json(response))
}
Err(_) => {
let stderr = String::from_utf8_lossy(&output.stderr);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "parse_failed",
"message": format!("Operation '{}' executed but response was not valid JSON.", req.operation_id),
"hint": if stderr.contains("not found") || stderr.contains("No operation") {
format!("Operation '{}' may not exist. Use /api/find or /api/operations to discover valid operations.", req.operation_id)
} else if stderr.contains("URL") || stderr.contains("url") || stderr.contains("base_url") {
"Base URL may not be configured. Start server with --base-url flag or set API_BASE_URL env var.".to_string()
} else if stderr.contains("localhost") || stderr.contains("loopback") {
"Localhost access may be blocked. Start server with --allow-localhost flag.".to_string()
} else {
"Check that the target API is running and the base URL is correct.".to_string()
},
"stderr": stderr.to_string(),
"raw": stdout.to_string(),
})),
)
}
}
}