Skip to main content

graphyn_mcp/
server.rs

1//! Graphyn MCP server — exposes graph query tools to coding agents.
2//!
3//! Uses the official `rmcp` SDK with stdio transport. Agents like Cursor
4//! and Claude Code spawn `graphyn serve --stdio` and communicate via
5//! JSON-RPC over stdin/stdout.
6
7use 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/// The Graphyn MCP server. Holds a loaded graph and tool router.
22#[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    /// Create a new server by loading the graph from `.graphyn/db`.
34    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    /// Given a symbol name, returns all symbols that depend on it and would
44    /// be affected by changes. Resolves aliases. Tracks property-level access.
45    #[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    /// Returns everything a given symbol depends on — its full dependency tree.
61    #[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    /// Finds all usages of a symbol across the codebase, including under
77    /// aliases and re-exports.
78    #[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    /// Rebuild and persist the graph index. Agents can call this after code changes.
94    #[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
139/// Start the MCP server over stdio transport.
140pub 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    // Wait until the client disconnects
147    running_service.waiting().await?;
148
149    Ok(())
150}
151
152// ── helpers ──────────────────────────────────────────────────
153
154fn 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}