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;
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, symbol_usages};
20
21/// The Graphyn MCP server. Holds a loaded graph and tool router.
22#[derive(Clone)]
23pub struct GraphynMcpServer {
24    graph: Arc<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(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        blast_radius::execute(&self.graph, params.0)
54    }
55
56    /// Returns everything a given symbol depends on — its full dependency tree.
57    #[tool(
58        name = "get_dependencies",
59        description = "Returns everything a given symbol depends on — its full dependency tree."
60    )]
61    async fn get_dependencies(
62        &self,
63        params: Parameters<dependencies::DependenciesParams>,
64    ) -> Result<String, String> {
65        dependencies::execute(&self.graph, params.0)
66    }
67
68    /// Finds all usages of a symbol across the codebase, including under
69    /// aliases and re-exports.
70    #[tool(
71        name = "get_symbol_usages",
72        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."
73    )]
74    async fn get_symbol_usages(
75        &self,
76        params: Parameters<symbol_usages::SymbolUsagesParams>,
77    ) -> Result<String, String> {
78        symbol_usages::execute(&self.graph, params.0)
79    }
80}
81
82#[tool_handler]
83impl ServerHandler for GraphynMcpServer {
84    fn get_info(&self) -> ServerInfo {
85        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
86            .with_server_info(rmcp::model::Implementation::new(
87                "graphyn",
88                env!("CARGO_PKG_VERSION"),
89            ))
90            .with_instructions(
91                "Graphyn is a code intelligence engine. Use get_blast_radius to find \
92                 what will break if you change a symbol, get_dependencies to see what \
93                 a symbol depends on, and get_symbol_usages to find every usage \
94                 including aliased imports.",
95            )
96    }
97}
98
99/// Start the MCP server over stdio transport.
100pub async fn serve_stdio(repo_root: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
101    let server = GraphynMcpServer::new(repo_root)?;
102
103    let transport = rmcp::transport::io::stdio();
104    let running_service = server.serve(transport).await?;
105
106    // Wait until the client disconnects
107    running_service.waiting().await?;
108
109    Ok(())
110}
111
112// ── helpers ──────────────────────────────────────────────────
113
114fn load_graph(repo_root: &Path) -> Result<GraphynGraph, String> {
115    let db = repo_root.join(".graphyn").join("db");
116    if !db.exists() {
117        return Err(format!(
118            "No graph found at {}. Run `graphyn analyze <path>` first.",
119            db.display(),
120        ));
121    }
122    let store = RocksGraphStore::open(&db).map_err(|e| format!("failed to open store: {e}"))?;
123    store
124        .load_graph()
125        .map_err(|e| format!("failed to load graph: {e}"))
126}