use std::env;
use std::io::Write;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use super::daemon::{DaemonClientHello, DaemonHello, MAX_HELLO_LINE_BYTES};
use super::server_instructions::SERVER_INSTRUCTIONS;
use super::session::{PROTOCOL_VERSION, server_info};
use super::tools::{ToolHandler, get_static_tools};
use super::version::code_graph_package_version;
pub const DEFAULT_PPID_POLL_MS: u64 = 5_000;
pub const HOST_PPID_ENV: &str = "RUSTCODEGRAPH_HOST_PPID";
pub const LOG_ATTACH_ENV: &str = "RUSTCODEGRAPH_MCP_LOG_ATTACH";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProxyOutcome {
Proxied,
FallbackNeeded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProxyResult {
pub outcome: ProxyOutcome,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
pub fn log_attached_daemon(socket_path: impl AsRef<Path>, hello: &DaemonHello) {
if env::var(LOG_ATTACH_ENV).ok().as_deref() != Some("1") {
return;
}
let _ = writeln!(
std::io::stderr(),
"[RustCodeGraph MCP] Attached to shared daemon on {} (pid {}, v{}).",
socket_path.as_ref().display(),
hello.pid,
hello.rustcodegraph
);
}
pub fn run_proxy(_socket_path: impl AsRef<Path>, _expected_version: Option<&str>) -> ProxyResult {
ProxyResult {
outcome: ProxyOutcome::FallbackNeeded,
reason: Some("Rust proxy socket runtime is not wired yet".to_string()),
}
}
pub fn connect_with_hello_result(
hello_line: &str,
expected_version: Option<&str>,
) -> Result<Option<DaemonHello>, String> {
let hello = read_hello_line_from_str(hello_line)?;
let expected = match expected_version {
Some(version) => version,
None => code_graph_package_version(),
};
if hello.rustcodegraph != expected {
return Ok(None);
}
Ok(Some(hello))
}
pub fn client_hello(host_ppid_env: Option<&str>, current_ppid: u32) -> DaemonClientHello {
DaemonClientHello {
rustcodegraph_client: 1,
pid: std::process::id(),
host_pid: parse_host_ppid(host_ppid_env).or((current_ppid > 1).then_some(current_ppid)),
}
}
pub fn client_hello_line(host_ppid_env: Option<&str>, current_ppid: u32) -> String {
let mut line = serde_json::to_string(&client_hello(host_ppid_env, current_ppid))
.unwrap_or_else(|_| "{}".to_string());
line.push('\n');
line
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalHandshakeDeps {
pub root: PathBuf,
}
pub fn local_initialize_response(id: Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": { "tools": {} },
"serverInfo": server_info(),
"instructions": SERVER_INSTRUCTIONS,
}
})
}
pub fn local_tools_list_response(id: Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"result": { "tools": get_static_tools() },
})
}
pub fn local_fallback_tool_result(
handler: &mut ToolHandler,
name: &str,
args: &serde_json::Map<String, Value>,
) -> Value {
json!(handler.execute(name, args))
}
pub fn read_hello_line_from_str(line: &str) -> Result<DaemonHello, String> {
if line.len() > MAX_HELLO_LINE_BYTES {
return Err("daemon hello line exceeded size limit".to_string());
}
let parsed: DaemonHello =
serde_json::from_str(line.trim()).map_err(|err| format!("daemon hello not JSON: {err}"))?;
if parsed.rustcodegraph.is_empty() || parsed.pid == 0 {
return Err("daemon hello missing required fields".to_string());
}
Ok(parsed)
}
pub fn parse_poll_ms(raw: Option<&str>) -> u64 {
let Some(raw) = raw else {
return DEFAULT_PPID_POLL_MS;
};
if raw.is_empty() {
return DEFAULT_PPID_POLL_MS;
}
raw.parse::<u64>().unwrap_or(DEFAULT_PPID_POLL_MS)
}
pub fn parse_host_ppid(raw: Option<&str>) -> Option<u32> {
let parsed = raw?.parse::<u32>().ok()?;
(parsed > 1).then_some(parsed)
}
pub fn host_ppid_from_env() -> Option<u32> {
parse_host_ppid(env::var(HOST_PPID_ENV).ok().as_deref())
}