Skip to main content
Glama

CodeGraph CLI MCP Server

by Jakedismo
graph_functions.rs30.2 kB
// ABOUTME: Rust SDK wrappers for SurrealDB graph analysis functions // ABOUTME: Provides type-safe interfaces for LLM-powered graph analysis tools use codegraph_core::{CodeGraphError, Result}; use serde::{Deserialize, Serialize}; #[cfg(test)] use serde_json::json; use std::sync::Arc; use surrealdb::{engine::any::Any, Surreal, Value as SurrealValue}; use tracing::{debug, error, warn}; /// Convert SurrealDB Value to clean serde_json::Value using accessor methods /// Avoids externally-tagged enum serialization that produces {"None": ...}, {"Number": {"Int": ...}} fn surreal_to_json(value: SurrealValue) -> serde_json::Value { // Use into_inner() to get the internal sql::Value, then serialize properly let inner = value.into_inner(); sql_value_to_json(inner) } fn sql_value_to_json(value: surrealdb::sql::Value) -> serde_json::Value { use surrealdb::sql::Value as SqlValue; match value { SqlValue::None | SqlValue::Null => serde_json::Value::Null, SqlValue::Bool(b) => serde_json::Value::Bool(b), SqlValue::Number(n) => { // Try to get as float first (handles both int and float) let f = n.as_float(); if f.fract() == 0.0 && f.abs() < i64::MAX as f64 { // It's effectively an integer serde_json::json!(f as i64) } else { serde_json::json!(f) } } SqlValue::Strand(s) => serde_json::Value::String(s.to_string()), SqlValue::Duration(d) => serde_json::Value::String(d.to_string()), SqlValue::Datetime(dt) => serde_json::Value::String(dt.to_string()), SqlValue::Uuid(u) => serde_json::Value::String(u.to_string()), SqlValue::Array(arr) => { serde_json::Value::Array(arr.into_iter().map(sql_value_to_json).collect()) } SqlValue::Object(obj) => { let map: serde_json::Map<String, serde_json::Value> = obj .into_iter() .map(|(k, v)| (k.to_string(), sql_value_to_json(v))) .collect(); serde_json::Value::Object(map) } SqlValue::Thing(thing) => serde_json::Value::String(thing.to_string()), SqlValue::Bytes(b) => serde_json::Value::String(format!("bytes:{}", b.len())), other => serde_json::Value::String(format!("{}", other)), } } /// Wrapper for SurrealDB graph analysis functions /// Provides type-safe Rust interfaces for calling SurrealDB functions #[derive(Clone)] pub struct GraphFunctions { db: Arc<Surreal<Any>>, project_id: String, } impl GraphFunctions { pub fn new(db: Arc<Surreal<Any>>) -> Self { Self { db, project_id: Self::default_project_id(), } } pub fn new_with_project_id(db: Arc<Surreal<Any>>, project_id: impl Into<String>) -> Self { Self { db, project_id: project_id.into(), } } pub fn project_id(&self) -> &str { &self.project_id } fn default_project_id() -> String { std::env::var("CODEGRAPH_PROJECT_ID") .ok() .filter(|v| !v.trim().is_empty()) .or_else(|| { std::env::current_dir() .ok() .map(|p| p.display().to_string()) }) .unwrap_or_else(|| "default-project".to_string()) } /// Get transitive dependencies of a node up to specified depth /// /// # Arguments /// * `node_id` - The ID of the node to analyze /// * `edge_type` - The type of edge to follow (e.g., "Calls", "Imports") /// * `depth` - Maximum depth to traverse (1-10, defaults to 3) /// /// # Returns /// Vector of nodes representing transitive dependencies pub async fn get_transitive_dependencies( &self, node_id: &str, edge_type: &str, depth: i32, ) -> Result<Vec<DependencyNode>> { debug!( "Calling fn::get_transitive_dependencies({}, {}, {}, project={})", node_id, edge_type, depth, self.project_id ); let result: Vec<DependencyNode> = self .db .query( "RETURN fn::get_transitive_dependencies($project_id, $node_id, $edge_type, $depth)", ) .bind(("project_id", self.project_id.clone())) .bind(("node_id", node_id.to_string())) .bind(("edge_type", edge_type.to_string())) .bind(("depth", depth)) .await .map_err(|e| { error!("Failed to call get_transitive_dependencies: {}", e); CodeGraphError::Database(format!("get_transitive_dependencies failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize dependencies: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Detect circular dependencies for a given edge type /// /// # Arguments /// * `edge_type` - The type of edge to analyze (e.g., "Imports", "Uses") /// /// # Returns /// Vector of circular dependency pairs (A <-> B relationships) pub async fn detect_circular_dependencies( &self, edge_type: &str, ) -> Result<Vec<CircularDependency>> { debug!( "Calling fn::detect_circular_dependencies({}, project={})", edge_type, self.project_id ); let result: Vec<CircularDependency> = self .db .query("RETURN fn::detect_circular_dependencies($project_id, $edge_type)") .bind(("project_id", self.project_id.clone())) .bind(("edge_type", edge_type.to_string())) .await .map_err(|e| { error!("Failed to call detect_circular_dependencies: {}", e); CodeGraphError::Database(format!("detect_circular_dependencies failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize circular dependencies: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Trace the call chain starting from a node /// /// # Arguments /// * `from_node` - The ID of the starting node /// * `max_depth` - Maximum depth to traverse (1-10, defaults to 5) /// /// # Returns /// Vector of nodes in the call chain with depth information pub async fn trace_call_chain( &self, from_node: &str, max_depth: i32, ) -> Result<Vec<CallChainNode>> { debug!( "Calling fn::trace_call_chain({}, {}, project={})", from_node, max_depth, self.project_id ); let result: Vec<CallChainNode> = self .db .query("RETURN fn::trace_call_chain($project_id, $from_node, $max_depth)") .bind(("project_id", self.project_id.clone())) .bind(("from_node", from_node.to_string())) .bind(("max_depth", max_depth)) .await .map_err(|e| { error!("Failed to call trace_call_chain: {}", e); CodeGraphError::Database(format!("trace_call_chain failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize call chain: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Calculate coupling metrics for a node /// /// # Arguments /// * `node_id` - The ID of the node to analyze /// /// # Returns /// Coupling metrics including afferent, efferent, and instability pub async fn calculate_coupling_metrics(&self, node_id: &str) -> Result<CouplingMetricsResult> { debug!( "Calling fn::calculate_coupling_metrics({}, project={})", node_id, self.project_id ); // Use Option to handle NONE returned by SurrealDB when node doesn't exist let results: Vec<Option<CouplingMetricsResult>> = self .db .query("RETURN fn::calculate_coupling_metrics($project_id, $node_id)") .bind(("project_id", self.project_id.clone())) .bind(("node_id", node_id.to_string())) .await .map_err(|e| { error!("Failed to call calculate_coupling_metrics: {}", e); CodeGraphError::Database(format!("calculate_coupling_metrics failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize coupling metrics: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; results .into_iter() .next() .flatten() .ok_or_else(|| { CodeGraphError::Database(format!( "Node not found or project_id mismatch: node_id='{}', expected project_id='{}'. \ Ensure the node exists and was indexed with the same project_id.", node_id, self.project_id )) }) } /// Get hub nodes with degree >= min_degree /// /// # Arguments /// * `min_degree` - Minimum total degree (defaults to 5) /// /// # Returns /// Vector of highly connected hub nodes sorted by degree (descending) pub async fn get_hub_nodes(&self, min_degree: i32) -> Result<Vec<HubNode>> { debug!( "Calling fn::get_hub_nodes({}, project={})", min_degree, self.project_id ); let result: Vec<HubNode> = self .db .query("RETURN fn::get_hub_nodes($project_id, $min_degree)") .bind(("project_id", self.project_id.clone())) .bind(("min_degree", min_degree)) .await .map_err(|e| { error!("Failed to call get_hub_nodes: {}", e); CodeGraphError::Database(format!("get_hub_nodes failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize hub nodes: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Get reverse dependencies (dependents) of a node /// /// # Arguments /// * `node_id` - The ID of the node to analyze /// * `edge_type` - The type of edge to follow /// * `depth` - Maximum depth to traverse (1-10, defaults to 3) /// /// # Returns /// Vector of nodes that depend on the target node pub async fn get_reverse_dependencies( &self, node_id: &str, edge_type: &str, depth: i32, ) -> Result<Vec<DependencyNode>> { debug!( "Calling fn::get_reverse_dependencies({}, {}, {}, project={})", node_id, edge_type, depth, self.project_id ); let result: Vec<DependencyNode> = self .db .query("RETURN fn::get_reverse_dependencies($project_id, $node_id, $edge_type, $depth)") .bind(("project_id", self.project_id.clone())) .bind(("node_id", node_id.to_string())) .bind(("edge_type", edge_type.to_string())) .bind(("depth", depth)) .await .map_err(|e| { error!("Failed to call get_reverse_dependencies: {}", e); CodeGraphError::Database(format!("get_reverse_dependencies failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize reverse dependencies: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Get complexity hotspots - functions with high cyclomatic complexity and coupling /// /// # Arguments /// * `min_complexity` - Minimum cyclomatic complexity threshold (default: 5.0) /// * `limit` - Maximum number of results (1-100, default: 20) /// /// # Returns /// Vector of complexity hotspots sorted by risk_score (complexity × afferent_coupling) pub async fn get_complexity_hotspots( &self, min_complexity: f32, limit: i32, ) -> Result<Vec<ComplexityHotspot>> { debug!( "Calling fn::get_complexity_hotspots(min={}, limit={}, project={})", min_complexity, limit, self.project_id ); let result: Vec<ComplexityHotspot> = self .db .query("RETURN fn::get_complexity_hotspots($project_id, $min_complexity, $limit)") .bind(("project_id", self.project_id.clone())) .bind(("min_complexity", min_complexity)) .bind(("limit", limit)) .await .map_err(|e| { error!("Failed to call get_complexity_hotspots: {}", e); CodeGraphError::Database(format!("get_complexity_hotspots failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize complexity hotspots: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Count nodes for the current project (used for health checks) pub async fn count_nodes_for_project(&self) -> Result<usize> { let mut response = self .db .query("SELECT VALUE count() FROM nodes WHERE project_id = $project_id") .bind(("project_id", self.project_id.clone())) .await .map_err(|e| { CodeGraphError::Database(format!("count_nodes_for_project query failed: {}", e)) })?; let count: Option<usize> = response .take(0) .map_err(|e| CodeGraphError::Database(format!("Failed to deserialize count: {}", e)))?; Ok(count.unwrap_or(0)) } /// Find nodes by (partial) name within the current project pub async fn find_nodes_by_name( &self, needle: &str, limit: usize, ) -> Result<Vec<NodeReference>> { let max = limit.clamp(1, 50) as i64; debug!( "Calling fn::find_nodes_by_name({}, project={}, limit={})", needle, self.project_id, max ); let result: Vec<NodeReference> = self .db .query("RETURN fn::find_nodes_by_name($project_id, $needle, $limit)") .bind(("project_id", self.project_id.clone())) .bind(("needle", needle.to_string())) .bind(("limit", max)) .await .map_err(|e| { error!("Failed to call find_nodes_by_name: {}", e); CodeGraphError::Database(format!("find_nodes_by_name failed: {}", e)) })? .take(0) .map_err(|e| { error!("Failed to deserialize find_nodes_by_name results: {}", e); CodeGraphError::Database(format!("Deserialization failed: {}", e)) })?; Ok(result) } /// Comprehensive semantic search with HNSW vector search, full-text, and graph enrichment /// /// Calls fn::semantic_search_with_context in SurrealDB which combines: /// - HNSW vector similarity search /// - Full-text search using code_analyzer /// - Graph enrichment with dependencies and file context /// /// # Parameters /// - `query_text`: Original search query /// - `query_embedding`: Pre-generated embedding vector /// - `dimension`: Embedding dimension (384,768,1024,1536,2048,2560,3072,4096) /// - `limit`: Maximum results /// - `threshold`: Minimum similarity score (0.0-1.0) /// - `include_graph_context`: Whether to enrich with graph data pub async fn semantic_search_with_context( &self, query_text: &str, query_embedding: &[f32], dimension: usize, limit: usize, threshold: f32, include_graph_context: bool, ) -> Result<Vec<serde_json::Value>> { let skip_chunking = std::env::var("CODEGRAPH_EMBEDDING_SKIP_CHUNKING") .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false); if !skip_chunking { return self .semantic_search_chunks_with_context( query_text, query_embedding, dimension, limit, threshold, include_graph_context, ) .await; } debug!( "Calling fn::semantic_search_with_context(project={}, query='{}', dim={}, limit={}, threshold={})", self.project_id, query_text, dimension, limit, threshold ); // Convert embedding to Value let embedding_value: serde_json::Value = serde_json::to_value(query_embedding).map_err(|e| { CodeGraphError::Database(format!("Failed to serialize embedding: {}", e)) })?; let mut response = self .db .query("RETURN fn::semantic_search_with_context($project_id, $query_embedding, $query_text, $dimension, $limit, $threshold, $include_graph_context)") .bind(("project_id", self.project_id.clone())) .bind(("query_embedding", embedding_value)) .bind(("query_text", query_text.to_string())) .bind(("dimension", dimension as i64)) .bind(("limit", limit as i64)) .bind(("threshold", threshold as f64)) .bind(("include_graph_context", include_graph_context)) .await .map_err(|e| { error!("Failed to call semantic_search_with_context: {}", e); CodeGraphError::Database(format!("semantic_search_with_context failed: {}", e)) })?; // Workaround for SurrealDB 2.x SDK bug (GitHub #4921): // Direct deserialization to serde_json::Value fails with "invalid type: enum" // Instead, get raw SurrealDB Value and serialize via serde_json::to_value let raw_value: SurrealValue = response.take(0).map_err(|e| { error!( "Failed to get raw value from semantic_search_with_context: {}", e ); CodeGraphError::Database(format!("Failed to get raw value: {}", e)) })?; // Convert SurrealDB Value to clean JSON (avoids enum variant tags) let json_value = surreal_to_json(raw_value); // Extract Vec from the JSON value let result: Vec<serde_json::Value> = match json_value { serde_json::Value::Array(arr) => arr, serde_json::Value::Null => Vec::new(), other => vec![other], }; Ok(result) } async fn semantic_search_chunks_with_context( &self, query_text: &str, query_embedding: &[f32], dimension: usize, limit: usize, threshold: f32, include_graph_context: bool, ) -> Result<Vec<serde_json::Value>> { debug!( "Calling fn::semantic_search_chunks_with_context(project={}, query='{}', dim={}, limit={}, threshold={})", self.project_id, query_text, dimension, limit, threshold ); let embedding_value: serde_json::Value = serde_json::to_value(query_embedding).map_err(|e| { CodeGraphError::Database(format!("Failed to serialize embedding: {}", e)) })?; let mut response = self .db .query("RETURN fn::semantic_search_chunks_with_context($project_id, $query_embedding, $query_text, $dimension, $limit, $threshold, $include_graph_context)") .bind(("project_id", self.project_id.clone())) .bind(("query_embedding", embedding_value)) .bind(("query_text", query_text.to_string())) .bind(("dimension", dimension as i64)) .bind(("limit", limit as i64)) .bind(("threshold", threshold as f64)) .bind(("include_graph_context", include_graph_context)) .await .map_err(|e| { let msg = e.to_string(); if msg.contains("semantic_search_chunks_with_context") { warn!( "semantic_search_chunks_with_context missing in DB; falling back to fn::semantic_search_with_context" ); CodeGraphError::Database("MISSING_CHUNK_FN".into()) } else { error!("Failed to call semantic_search_chunks_with_context: {}", msg); CodeGraphError::Database(format!("semantic_search_chunks_with_context failed: {}", msg)) } })?; // Workaround for SurrealDB 2.x SDK bug (GitHub #4921): // Direct deserialization to serde_json::Value fails with "invalid type: enum" // Instead, get raw SurrealDB Value and serialize via serde_json::to_value let raw_value: SurrealValue = response.take(0).map_err(|e| { error!( "Failed to get raw value from semantic_search_chunks_with_context: {}", e ); CodeGraphError::Database(format!("Failed to get raw value: {}", e)) })?; // Convert SurrealDB Value to clean JSON (avoids enum variant tags) let json_value = surreal_to_json(raw_value); // Extract Vec from the JSON value let result: Vec<serde_json::Value> = match json_value { serde_json::Value::Array(arr) => arr, serde_json::Value::Null => Vec::new(), other => vec![other], }; Ok(result) } /// Semantic search that returns full node records with content, deduplicated from chunk matches. /// Uses chunk-level semantic search for precision, then deduplicates by parent node. /// Returns complete code units (functions, classes, etc.) with full content for context-engineering. pub async fn semantic_search_nodes_via_chunks( &self, query_text: &str, query_embedding: &[f32], dimension: usize, limit: usize, threshold: f32, ) -> Result<Vec<serde_json::Value>> { debug!( "Calling fn::semantic_search_nodes_via_chunks(project={}, query='{}', dim={}, limit={}, threshold={})", self.project_id, query_text, dimension, limit, threshold ); let embedding_value: serde_json::Value = serde_json::to_value(query_embedding).map_err(|e| { CodeGraphError::Database(format!("Failed to serialize embedding: {}", e)) })?; let mut response = self .db .query("RETURN fn::semantic_search_nodes_via_chunks($project_id, $query_embedding, $query_text, $dimension, $limit, $threshold)") .bind(("project_id", self.project_id.clone())) .bind(("query_embedding", embedding_value)) .bind(("query_text", query_text.to_string())) .bind(("dimension", dimension as i64)) .bind(("limit", limit as i64)) .bind(("threshold", threshold as f64)) .await .map_err(|e| { error!("Failed to call semantic_search_nodes_via_chunks: {}", e); CodeGraphError::Database(format!("semantic_search_nodes_via_chunks failed: {}", e)) })?; let raw_value: SurrealValue = response.take(0).map_err(|e| { error!( "Failed to get raw value from semantic_search_nodes_via_chunks: {}", e ); CodeGraphError::Database(format!("Failed to get raw value: {}", e)) })?; // Convert SurrealDB Value to clean JSON (avoids enum variant tags) let json_value = surreal_to_json(raw_value); // Extract Vec from the JSON value let result: Vec<serde_json::Value> = match json_value { serde_json::Value::Array(arr) => arr, serde_json::Value::Null => Vec::new(), other => vec![other], }; Ok(result) } } // ============================================================================ // Type Definitions for Function Results // ============================================================================ /// Node with dependency depth information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DependencyNode { pub id: String, pub name: String, pub kind: Option<String>, pub location: Option<NodeLocation>, pub language: Option<String>, pub content: Option<String>, pub metadata: Option<serde_json::Value>, pub dependency_depth: Option<i32>, pub dependent_depth: Option<i32>, } /// Circular dependency pair #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CircularDependency { pub node1_id: String, pub node2_id: String, pub node1: NodeInfo, pub node2: NodeInfo, pub dependency_type: String, } /// Call chain node with caller information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CallChainNode { pub id: String, pub name: String, pub kind: Option<String>, pub location: Option<NodeLocation>, pub language: Option<String>, pub content: Option<String>, pub metadata: Option<serde_json::Value>, pub call_depth: Option<i32>, pub called_by: Option<Vec<CallerInfo>>, } /// Coupling metrics result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CouplingMetricsResult { pub node: NodeInfo, pub metrics: CouplingMetrics, pub dependents: Vec<NodeReference>, pub dependencies: Vec<NodeReference>, } /// Coupling metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CouplingMetrics { pub afferent_coupling: i32, pub efferent_coupling: i32, pub total_coupling: i32, pub instability: f64, pub stability: f64, pub is_stable: bool, pub is_unstable: bool, pub coupling_category: String, } /// Hub node with degree information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HubNode { pub node_id: String, pub node: NodeInfo, pub afferent_degree: i32, pub efferent_degree: i32, pub total_degree: i32, pub incoming_by_type: Vec<EdgeTypeCount>, pub outgoing_by_type: Vec<EdgeTypeCount>, } /// Edge type count #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EdgeTypeCount { pub edge_type: String, pub count: i32, } /// Node information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeInfo { pub id: String, pub name: String, pub kind: Option<String>, pub location: Option<NodeLocation>, pub language: Option<String>, pub content: Option<String>, pub metadata: Option<serde_json::Value>, } /// Node reference (minimal info) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeReference { pub id: String, pub name: String, pub kind: Option<String>, pub location: Option<NodeLocation>, } /// Caller information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CallerInfo { pub id: String, pub name: String, pub kind: Option<String>, } /// Node location information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeLocation { pub file_path: String, pub start_line: Option<i32>, pub end_line: Option<i32>, } /// Complexity hotspot with risk metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplexityHotspot { pub id: String, pub name: String, pub kind: Option<String>, pub language: Option<String>, pub file_path: Option<String>, pub start_line: Option<i32>, pub end_line: Option<i32>, pub complexity: f32, pub afferent_coupling: i32, pub efferent_coupling: i32, pub instability: f32, pub risk_score: f32, } #[cfg(test)] mod tests { use super::*; #[test] fn test_dependency_node_serialization() { let node = DependencyNode { id: "test:1".to_string(), name: "test_function".to_string(), kind: Some("function".to_string()), location: Some(NodeLocation { file_path: "test.rs".to_string(), start_line: Some(10), end_line: Some(20), }), language: Some("rust".to_string()), content: None, metadata: None, dependency_depth: Some(1), dependent_depth: None, }; let json = serde_json::to_string(&node).unwrap(); assert!(json.contains("test_function")); } #[cfg(feature = "surrealdb")] #[tokio::test] async fn count_nodes_for_project_filters_by_project() { use surrealdb::opt::auth::Root; let db: Surreal<Any> = Surreal::init(); db.connect("mem://").await.unwrap(); db.use_ns("test").use_db("test").await.unwrap(); db.signin(Root { username: "root", password: "root", }) .await .ok(); // mem engine ignores auth // Two projects, only one should be counted db.query("CREATE nodes CONTENT $doc") .bind(( "doc", json!({ "id": "nodes:a1", "name": "A1", "project_id": "proj-a" }), )) .await .unwrap(); db.query("CREATE nodes CONTENT $doc") .bind(( "doc", json!({ "id": "nodes:b1", "name": "B1", "project_id": "proj-b" }), )) .await .unwrap(); let gf = GraphFunctions::new_with_project_id(Arc::new(db), "proj-a"); let count = gf.count_nodes_for_project().await.unwrap(); assert_eq!(count, 1, "Should only count nodes in proj-a"); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/codegraph-rust'

If you have feedback or need assistance with the MCP directory API, please join our Discord server