use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use clap::Args;
use codeprysm_core::builder::{BuilderConfig, GraphBuilder};
use codeprysm_core::lazy::partitioner::GraphPartitioner;
use codeprysm_mcp::{PrismServer, ServerConfig};
use rmcp::{ServiceExt, transport::stdio};
use tokio::signal;
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
use crate::GlobalOptions;
#[derive(Args, Debug)]
pub struct McpArgs {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
codeprysm_dir: Option<PathBuf>,
#[arg(long)]
repo_id: Option<String>,
#[arg(long)]
queries: Option<PathBuf>,
#[arg(long)]
no_auto_generate: bool,
#[arg(long)]
log_file: Option<PathBuf>,
#[arg(long)]
debug: bool,
}
pub async fn execute(args: McpArgs, global: GlobalOptions) -> Result<()> {
let log_level = if args.debug || global.verbose {
Level::DEBUG
} else if global.quiet {
Level::ERROR
} else {
Level::INFO
};
if let Some(ref log_file) = args.log_file {
let file = std::fs::File::create(log_file)
.with_context(|| format!("Failed to create log file: {}", log_file.display()))?;
let subscriber = FmtSubscriber::builder()
.with_max_level(log_level)
.with_writer(file)
.with_ansi(false)
.finish();
tracing::subscriber::set_global_default(subscriber)
.context("Failed to set tracing subscriber")?;
} else {
let subscriber = FmtSubscriber::builder()
.with_max_level(log_level)
.with_writer(std::io::stderr)
.with_ansi(false)
.finish();
tracing::subscriber::set_global_default(subscriber)
.context("Failed to set tracing subscriber")?;
}
let root_path = args
.root
.or_else(|| global.workspace.as_ref().map(PathBuf::from))
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let root_path = root_path
.canonicalize()
.unwrap_or_else(|_| root_path.clone());
let codeprysm_dir = args
.codeprysm_dir
.unwrap_or_else(|| root_path.join(".codeprysm"));
if !codeprysm_dir.exists() {
std::fs::create_dir_all(&codeprysm_dir).with_context(|| {
format!(
"Failed to create codeprysm directory: {}",
codeprysm_dir.display()
)
})?;
info!("Created codeprysm directory: {}", codeprysm_dir.display());
}
let manifest_path = codeprysm_dir.join("manifest.json");
info!("Starting CodePrysm MCP Server");
info!(" Root: {}", root_path.display());
info!(" CodePrysm dir: {}", codeprysm_dir.display());
info!(" Qdrant: {}", global.qdrant_url);
if !root_path.exists() {
anyhow::bail!("Root path does not exist: {}", root_path.display());
}
if !manifest_path.exists() {
if args.no_auto_generate {
anyhow::bail!(
"Graph not found: {}. Remove --no-auto-generate to auto-generate.",
manifest_path.display()
);
}
info!("Graph not found, generating...");
generate_graph(&root_path, &codeprysm_dir, args.queries.as_deref())?;
info!("Graph generated successfully");
}
let mut config = ServerConfig::new(&root_path)
.with_qdrant_url(&global.qdrant_url)
.with_codeprysm_dir(&codeprysm_dir);
if let Some(repo_id) = args.repo_id {
config = config.with_repo_id(repo_id);
}
if let Some(ref queries) = args.queries {
config = config.with_queries_path(queries);
}
let server = PrismServer::new(config)
.await
.context("Failed to create MCP server")?;
info!("Server initialized, starting MCP protocol over stdio");
let server_for_shutdown = server.clone();
let service = server
.serve(stdio())
.await
.context("Failed to start MCP service")?;
tokio::select! {
result = service.waiting() => {
if let Err(e) = result {
info!("Service ended with error: {}", e);
} else {
info!("Service ended normally");
}
}
_ = shutdown_signal() => {
info!("Shutdown signal received");
server_for_shutdown.shutdown();
}
}
info!("Server shutdown complete");
Ok(())
}
fn generate_graph(
root_path: &Path,
codeprysm_dir: &Path,
queries_dir: Option<&Path>,
) -> Result<()> {
if !codeprysm_dir.exists() {
std::fs::create_dir_all(codeprysm_dir).with_context(|| {
format!(
"Failed to create codeprysm directory: {}",
codeprysm_dir.display()
)
})?;
}
let config = BuilderConfig::default();
let mut builder = match queries_dir {
Some(dir) => {
info!("Using custom queries directory: {}", dir.display());
GraphBuilder::with_config(dir, config).with_context(|| {
format!(
"Failed to create graph builder with queries from {}",
dir.display()
)
})?
}
None => {
info!("Using embedded queries (compiled into binary)");
GraphBuilder::with_embedded_queries(config)
}
};
info!("Building workspace graph from: {}", root_path.display());
let (graph, roots) = builder
.build_from_workspace(root_path)
.context("Failed to build graph")?;
info!("Discovered {} code root(s):", roots.len());
for root in &roots {
info!(
" - {} ({}) at {}",
root.name,
if root.is_git() { "git" } else { "code" },
root.relative_path
);
}
let root_name = root_path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "default".to_string());
let (_, stats) =
GraphPartitioner::partition_with_stats(&graph, codeprysm_dir, Some(&root_name))
.context("Failed to partition graph")?;
info!(
"Graph saved: {} nodes, {} partitions, {} cross-partition edges",
stats.total_nodes, stats.partition_count, stats.cross_partition_edges
);
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}