use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::ServerCapabilities;
use rmcp::model::ServerInfo;
use rmcp::{tool, tool_handler, tool_router, ServerHandler, ServiceExt};
use graphyn_core::graph::GraphynGraph;
use graphyn_store::RocksGraphStore;
use crate::tools::{blast_radius, dependencies, refresh_graph, symbol_usages};
#[derive(Clone)]
pub struct GraphynMcpServer {
graph: Arc<RwLock<GraphynGraph>>,
#[allow(dead_code)]
repo_root: PathBuf,
#[allow(dead_code)]
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl GraphynMcpServer {
pub fn new(repo_root: PathBuf) -> Result<Self, String> {
let graph = load_graph(&repo_root)?;
Ok(Self {
graph: Arc::new(RwLock::new(graph)),
repo_root,
tool_router: Self::tool_router(),
})
}
#[tool(
name = "get_blast_radius",
description = "Given a symbol name, returns all symbols that depend on it and would be affected by changes. Resolves aliases. Tracks property-level access."
)]
async fn get_blast_radius(
&self,
params: Parameters<blast_radius::BlastRadiusParams>,
) -> Result<String, String> {
let graph = self
.graph
.read()
.map_err(|_| "graph lock poisoned".to_string())?;
blast_radius::execute(&graph, params.0)
}
#[tool(
name = "get_dependencies",
description = "Returns everything a given symbol depends on — its full dependency tree."
)]
async fn get_dependencies(
&self,
params: Parameters<dependencies::DependenciesParams>,
) -> Result<String, String> {
let graph = self
.graph
.read()
.map_err(|_| "graph lock poisoned".to_string())?;
dependencies::execute(&graph, params.0)
}
#[tool(
name = "get_symbol_usages",
description = "Finds all usages of a symbol across the codebase, including under aliases and re-exports. Use this when you need to find all references before renaming or deleting a symbol."
)]
async fn get_symbol_usages(
&self,
params: Parameters<symbol_usages::SymbolUsagesParams>,
) -> Result<String, String> {
let graph = self
.graph
.read()
.map_err(|_| "graph lock poisoned".to_string())?;
symbol_usages::execute(&graph, params.0)
}
#[tool(
name = "refresh_graph_index",
description = "Re-analyzes the repository and updates the persisted graph index. Supports include/exclude filters and .gitignore respect."
)]
async fn refresh_graph_index(
&self,
params: Parameters<refresh_graph::RefreshGraphParams>,
) -> Result<String, String> {
let (new_graph, result) = refresh_graph::execute(&self.repo_root, params.0)?;
{
let mut graph = self
.graph
.write()
.map_err(|_| "graph lock poisoned".to_string())?;
*graph = new_graph;
}
Ok(format!(
"Graph index refreshed successfully.\nFiles indexed: {}\nSymbols: {}\nRelationships: {}\nAlias chains: {}\nParse errors: {}",
result.files_indexed,
result.symbols,
result.relationships,
result.alias_chains,
result.parse_errors
))
}
}
#[tool_handler]
impl ServerHandler for GraphynMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(rmcp::model::Implementation::new(
"graphyn",
env!("CARGO_PKG_VERSION"),
))
.with_instructions(
"Graphyn is a code intelligence engine. Use get_blast_radius to find \
what will break if you change a symbol, get_dependencies to see what \
a symbol depends on, get_symbol_usages to find every usage \
including aliased imports, and refresh_graph_index after repository changes.",
)
}
}
pub async fn serve_stdio(repo_root: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let server = GraphynMcpServer::new(repo_root)?;
let transport = rmcp::transport::io::stdio();
let running_service = server.serve(transport).await?;
running_service.waiting().await?;
Ok(())
}
fn load_graph(repo_root: &Path) -> Result<GraphynGraph, String> {
let db = repo_root.join(".graphyn").join("db");
if !db.exists() {
return Err(format!(
"No graph found at {}. Run `graphyn analyze <path>` first.",
db.display(),
));
}
let store = RocksGraphStore::open(&db).map_err(|e| format!("failed to open store: {e}"))?;
store
.load_graph()
.map_err(|e| format!("failed to load graph: {e}"))
}