use std::io::{self, BufRead, Write as _};
use std::sync::Arc;
use clap::Parser;
use tokio::sync::Mutex;
use agentic_reality_mcp::config::{self, ServerConfig};
use agentic_reality_mcp::protocol::ProtocolHandler;
use agentic_reality_mcp::session::SessionManager;
const MAX_CONTENT_LENGTH_BYTES: usize = 8 * 1024 * 1024;
const CONTENT_LENGTH_HEADER_PREFIX: &str = "content-length:";
#[derive(Parser)]
#[command(
name = "agentic-reality-mcp",
version,
about = "MCP server for AgenticReality"
)]
struct Cli {
#[arg(long, default_value = "stdio")]
mode: String,
#[arg(long, default_value = "3010")]
port: u16,
#[arg(long)]
data_dir: Option<String>,
}
fn main() {
let cli = Cli::parse();
let mut server_config = config::load_config();
server_config.mode = cli.mode.clone();
server_config.port = cli.port;
if cli.data_dir.is_some() {
server_config.data_dir = cli.data_dir.clone();
}
init_stderr_logging(&server_config.log_level);
tracing::info!(
mode = %server_config.mode,
port = server_config.port,
"AgenticReality MCP server starting"
);
match server_config.mode.as_str() {
"stdio" => {
if let Err(e) = run_stdio(&server_config) {
tracing::error!(error = %e, "stdio server error");
std::process::exit(1);
}
}
"http" => {
tracing::info!(
port = server_config.port,
"HTTP/SSE mode not yet implemented"
);
eprintln!(
"AgenticReality MCP server: HTTP mode on port {} (not yet implemented)",
server_config.port
);
}
other => {
tracing::error!(mode = %other, "unknown server mode");
eprintln!(
"error: unknown mode '{}', expected 'stdio' or 'http'",
other
);
std::process::exit(1);
}
}
}
fn init_stderr_logging(level: &str) {
let filter = match level {
"trace" => tracing::Level::TRACE,
"debug" => tracing::Level::DEBUG,
"warn" => tracing::Level::WARN,
"error" => tracing::Level::ERROR,
_ => tracing::Level::INFO,
};
let subscriber = tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_max_level(filter)
.with_target(false)
.compact()
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
}
fn run_stdio(config: &ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("failed to build tokio runtime: {}", e))?;
rt.block_on(async {
let mut session = SessionManager::new();
if let Some(ref dir) = config.data_dir {
let data_path = config::resolve_data_path(Some(dir));
let areal_path = data_path.join("reality.areal");
session = SessionManager::with_path(areal_path.to_string_lossy().to_string());
session.set_autosave(config.autosave);
match session.load() {
Ok(true) => tracing::info!("loaded existing session data"),
Ok(false) => tracing::debug!("no existing session file found"),
Err(e) => tracing::warn!(error = %e, "failed to load session data, starting fresh"),
}
}
let session = Arc::new(Mutex::new(session));
let handler = ProtocolHandler::new(session.clone());
let stdin = io::stdin();
let reader = stdin.lock();
let stdout = io::stdout();
for line_result in reader.lines() {
let line = match line_result {
Ok(l) => l,
Err(e) => {
tracing::error!(error = %e, "failed to read from stdin");
break;
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed
.to_ascii_lowercase()
.starts_with(CONTENT_LENGTH_HEADER_PREFIX)
{
continue;
}
if trimmed.len() > MAX_CONTENT_LENGTH_BYTES {
let error_response = serde_json::json!({
"jsonrpc": "2.0",
"id": serde_json::Value::Null,
"error": {
"code": -32600,
"message": format!("Request exceeds {} bytes", MAX_CONTENT_LENGTH_BYTES),
}
});
write_response(&stdout, &error_response);
continue;
}
let value: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(e) => {
let error_response = serde_json::json!({
"jsonrpc": "2.0",
"id": serde_json::Value::Null,
"error": {
"code": -32700,
"message": format!("Parse error: {}", e),
}
});
write_response(&stdout, &error_response);
continue;
}
};
let is_notification = value.get("id").is_none();
let method = value
.get("method")
.and_then(|m| m.as_str())
.map_or(String::new(), |m| m.to_string());
if is_notification && method == "notifications/initialized" {
tracing::info!("client confirmed initialization");
continue;
}
if is_notification && method == "notifications/cancelled" {
tracing::info!("client sent cancellation");
continue;
}
let response = handler.handle_request(value).await;
write_response(&stdout, &response);
if method == "initialize" {
tracing::info!("initialization handshake sent");
}
if method == "tools/call" {
let mut session_guard = session.lock().await;
if let Err(e) = session_guard.autosave_if_dirty() {
tracing::warn!(error = %e, "autosave failed");
}
}
}
{
let mut session_guard = session.lock().await;
if session_guard.is_dirty() {
match session_guard.save() {
Ok(true) => tracing::info!("session saved on shutdown"),
Ok(false) => {}
Err(e) => tracing::warn!(error = %e, "failed to save session on shutdown"),
}
}
}
tracing::info!("MCP server shutting down cleanly");
Ok(())
})
}
fn write_response(stdout: &io::Stdout, response: &serde_json::Value) {
let mut handle = stdout.lock();
match serde_json::to_string(response) {
Ok(json_str) => {
let _ = handle.write_all(json_str.as_bytes());
let _ = handle.write_all(b"\n");
let _ = handle.flush();
}
Err(e) => {
tracing::error!(error = %e, "failed to serialize response");
}
}
}