1use std::path::{Path, PathBuf};
8use std::sync::{Arc, RwLock};
9
10use rmcp::handler::server::router::tool::ToolRouter;
11use rmcp::handler::server::wrapper::Parameters;
12use rmcp::model::ServerCapabilities;
13use rmcp::model::ServerInfo;
14use rmcp::{tool, tool_handler, tool_router, ServerHandler, ServiceExt};
15
16use graphyn_core::graph::GraphynGraph;
17use graphyn_store::RocksGraphStore;
18
19use crate::tools::{blast_radius, dependencies, refresh_graph, symbol_usages};
20
21#[derive(Clone)]
23pub struct GraphynMcpServer {
24 graph: Arc<RwLock<GraphynGraph>>,
25 #[allow(dead_code)]
26 repo_root: PathBuf,
27 #[allow(dead_code)]
28 tool_router: ToolRouter<Self>,
29}
30
31#[tool_router]
32impl GraphynMcpServer {
33 pub fn new(repo_root: PathBuf) -> Result<Self, String> {
35 let graph = load_graph(&repo_root)?;
36 Ok(Self {
37 graph: Arc::new(RwLock::new(graph)),
38 repo_root,
39 tool_router: Self::tool_router(),
40 })
41 }
42
43 #[tool(
46 name = "get_blast_radius",
47 description = "Given a symbol name, returns all symbols that depend on it and would be affected by changes. Resolves aliases. Tracks property-level access."
48 )]
49 async fn get_blast_radius(
50 &self,
51 params: Parameters<blast_radius::BlastRadiusParams>,
52 ) -> Result<String, String> {
53 let graph = self
54 .graph
55 .read()
56 .map_err(|_| "graph lock poisoned".to_string())?;
57 blast_radius::execute(&graph, params.0)
58 }
59
60 #[tool(
62 name = "get_dependencies",
63 description = "Returns everything a given symbol depends on — its full dependency tree."
64 )]
65 async fn get_dependencies(
66 &self,
67 params: Parameters<dependencies::DependenciesParams>,
68 ) -> Result<String, String> {
69 let graph = self
70 .graph
71 .read()
72 .map_err(|_| "graph lock poisoned".to_string())?;
73 dependencies::execute(&graph, params.0)
74 }
75
76 #[tool(
79 name = "get_symbol_usages",
80 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."
81 )]
82 async fn get_symbol_usages(
83 &self,
84 params: Parameters<symbol_usages::SymbolUsagesParams>,
85 ) -> Result<String, String> {
86 let graph = self
87 .graph
88 .read()
89 .map_err(|_| "graph lock poisoned".to_string())?;
90 symbol_usages::execute(&graph, params.0)
91 }
92
93 #[tool(
95 name = "refresh_graph_index",
96 description = "Re-analyzes the repository and updates the persisted graph index. Supports include/exclude filters and .gitignore respect."
97 )]
98 async fn refresh_graph_index(
99 &self,
100 params: Parameters<refresh_graph::RefreshGraphParams>,
101 ) -> Result<String, String> {
102 let (new_graph, result) = refresh_graph::execute(&self.repo_root, params.0)?;
103 {
104 let mut graph = self
105 .graph
106 .write()
107 .map_err(|_| "graph lock poisoned".to_string())?;
108 *graph = new_graph;
109 }
110
111 Ok(format!(
112 "Graph index refreshed successfully.\nFiles indexed: {}\nSymbols: {}\nRelationships: {}\nAlias chains: {}\nParse errors: {}",
113 result.files_indexed,
114 result.symbols,
115 result.relationships,
116 result.alias_chains,
117 result.parse_errors
118 ))
119 }
120}
121
122#[tool_handler]
123impl ServerHandler for GraphynMcpServer {
124 fn get_info(&self) -> ServerInfo {
125 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
126 .with_server_info(rmcp::model::Implementation::new(
127 "graphyn",
128 env!("CARGO_PKG_VERSION"),
129 ))
130 .with_instructions(
131 "Graphyn is a code intelligence engine. Use get_blast_radius to find \
132 what will break if you change a symbol, get_dependencies to see what \
133 a symbol depends on, get_symbol_usages to find every usage \
134 including aliased imports, and refresh_graph_index after repository changes.",
135 )
136 }
137}
138
139pub async fn serve_stdio(repo_root: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
141 let server = GraphynMcpServer::new(repo_root)?;
142
143 let transport = rmcp::transport::io::stdio();
144 let running_service = server.serve(transport).await?;
145
146 running_service.waiting().await?;
148
149 Ok(())
150}
151
152fn load_graph(repo_root: &Path) -> Result<GraphynGraph, String> {
155 let db = repo_root.join(".graphyn").join("db");
156 if !db.exists() {
157 return Err(format!(
158 "No graph found at {}. Run `graphyn analyze <path>` first.",
159 db.display(),
160 ));
161 }
162 let store = RocksGraphStore::open(&db).map_err(|e| format!("failed to open store: {e}"))?;
163 store
164 .load_graph()
165 .map_err(|e| format!("failed to load graph: {e}"))
166}