use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{Context, Result, bail};
use rqmd_core::RqmdStore;
use crate::cli::{McpAction, McpArgs};
use crate::state::IndexState;
const DEFAULT_HTTP_PORT: u16 = 8181;
pub async fn run(args: McpArgs, state: &mut IndexState) -> Result<()> {
if let Some(McpAction::Stop) = args.action {
return stop();
}
if args.daemon {
return start_daemon(&args, state);
}
let options = state.rqmd_store_options()?;
let store = RqmdStore::open(options).context("opening index for the MCP server")?;
if args.http {
let port = args.port.unwrap_or(DEFAULT_HTTP_PORT);
rqmd_mcp::serve_http(port, store, false).await?;
} else {
rqmd_mcp::serve_stdio(store).await?;
}
Ok(())
}
fn pid_path() -> PathBuf {
rqmd_core::paths::cache_dir().join("mcp.pid")
}
fn log_path() -> PathBuf {
rqmd_core::paths::cache_dir().join("mcp.log")
}
fn start_daemon(args: &McpArgs, state: &mut IndexState) -> Result<()> {
let port = args.port.unwrap_or(DEFAULT_HTTP_PORT);
let pid_file = pid_path();
if let Some(pid) = read_pid(&pid_file) {
if is_alive(pid) {
bail!("Already running (PID {pid}). Run 'rqmd mcp stop' first.");
}
let _ = fs::remove_file(&pid_file);
}
if let Some(parent) = pid_file.parent() {
fs::create_dir_all(parent).ok();
}
let log = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(log_path())
.context("opening mcp.log")?;
let log_err = log.try_clone().context("cloning mcp.log handle")?;
let exe = std::env::current_exe().context("resolving rqmd executable")?;
let mut cmd = Command::new(exe);
cmd.arg("--index").arg(state.index_name());
cmd.args(["mcp", "--http", "--port"]).arg(port.to_string());
cmd.stdin(Stdio::null())
.stdout(Stdio::from(log))
.stderr(Stdio::from(log_err));
detach(&mut cmd);
make_std_handles_noninheritable();
let child = cmd.spawn().context("spawning mcp daemon")?;
fs::write(&pid_file, child.id().to_string()).context("writing PID file")?;
println!(
"Started on http://localhost:{port}/mcp (PID {})",
child.id()
);
Ok(())
}
fn stop() -> Result<()> {
let pid_file = pid_path();
let Some(pid) = read_pid(&pid_file) else {
println!("No MCP daemon running (no PID file).");
return Ok(());
};
if is_alive(pid) {
kill(pid);
let _ = fs::remove_file(&pid_file);
println!("Stopped rqmd MCP server (PID {pid}).");
} else {
let _ = fs::remove_file(&pid_file);
println!("Cleaned up stale PID file (server was not running).");
}
Ok(())
}
fn read_pid(path: &Path) -> Option<u32> {
fs::read_to_string(path).ok()?.trim().parse::<u32>().ok()
}
pub(crate) fn running_daemon_pid() -> Option<u32> {
let pid = read_pid(&pid_path())?;
is_alive(pid).then_some(pid)
}
#[cfg(windows)]
fn detach(cmd: &mut Command) {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}
#[cfg(unix)]
fn detach(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
#[cfg(windows)]
fn make_std_handles_noninheritable() {
use windows_sys::Win32::Foundation::{HANDLE_FLAG_INHERIT, SetHandleInformation};
use windows_sys::Win32::System::Console::{
GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
};
unsafe {
for id in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
let h = GetStdHandle(id);
if !h.is_null() {
SetHandleInformation(h, HANDLE_FLAG_INHERIT, 0);
}
}
}
}
#[cfg(not(windows))]
fn make_std_handles_noninheritable() {}
fn is_alive(pid: u32) -> bool {
use sysinfo::{Pid, System};
let sys = System::new_all();
sys.process(Pid::from_u32(pid)).is_some()
}
fn kill(pid: u32) -> bool {
use sysinfo::{Pid, Signal, System};
let sys = System::new_all();
match sys.process(Pid::from_u32(pid)) {
Some(p) => p.kill_with(Signal::Term).unwrap_or_else(|| p.kill()),
None => false,
}
}