Skip to main content
Glama

CodeGraph CLI MCP Server

by Jakedismo
handlers.rs9.68 kB
use crate::{ApiError, ApiResult, AppState}; use axum::{ extract::{Path, Query, State}, http::{ header::{CACHE_CONTROL, ETAG}, HeaderMap, HeaderValue, StatusCode, }, Json, }; use codegraph_core::GraphStore; // Import GraphStore trait use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; #[derive(Serialize, ToSchema)] pub struct LocationDto { pub file_path: String, pub line: u32, pub column: u32, pub end_line: Option<u32>, pub end_column: Option<u32>, } // -------- Index -------- #[derive(Deserialize, ToSchema)] pub struct IndexRequest { /// Directory path to index (repository/workspace root) pub path: String, /// Use parallel parsing (default: true) pub parallel: Option<bool>, } #[derive(Serialize, ToSchema)] pub struct IndexResponse { pub nodes_indexed: usize, pub files_parsed: usize, pub total_files: usize, pub total_lines: usize, pub duration_ms: u64, pub message: String, } /// Index a source tree (repository/workspace directory) #[utoipa::path( post, path = "/v1/index", tag = "v1", request_body = IndexRequest, responses( (status = 200, description = "Indexing completed", body = IndexResponse), (status = 400, description = "Invalid input"), (status = 500, description = "Internal error") ) )] pub async fn post_index( State(state): State<AppState>, Json(req): Json<IndexRequest>, ) -> ApiResult<Json<IndexResponse>> { if req.path.trim().is_empty() { return Err(ApiError::Validation("path must not be empty".into())); } // Validate directory exists let meta = tokio::fs::metadata(&req.path) .await .map_err(|_| ApiError::Validation(format!("path does not exist: {}", req.path)))?; if !meta.is_dir() { return Err(ApiError::Validation(format!( "path is not a directory: {}", req.path ))); } let parallel = req.parallel.unwrap_or(true); let (nodes, stats) = if parallel { state .parser .parse_directory_parallel(&req.path) .await .map_err(ApiError::CodeGraph)? } else { // Fallback to parallel API if non-parallel is not implemented state .parser .parse_directory_parallel(&req.path) .await .map_err(ApiError::CodeGraph)? }; let nodes_count = nodes.len(); let mut graph = state.graph.write().await; for node in nodes { graph.add_node(node).await.map_err(ApiError::CodeGraph)?; } let duration_ms = stats.parsing_duration.as_millis() as u64; Ok(Json(IndexResponse { nodes_indexed: nodes_count, files_parsed: stats.parsed_files, total_files: stats.total_files, total_lines: stats.total_lines, duration_ms, message: format!( "Indexed {} nodes from {} files", nodes_count, stats.parsed_files ), })) } // -------- Search -------- #[derive(Deserialize, IntoParams, ToSchema)] pub struct SearchRequest { /// Free-text query string pub q: String, /// Maximum results to return (1..=100) pub limit: Option<usize>, } #[derive(Serialize, ToSchema)] pub struct SearchItem { pub node_id: String, pub score: f32, pub name: String, pub node_type: String, pub language: String, pub file_path: String, } #[derive(Serialize, ToSchema)] pub struct SearchResponse { pub results: Vec<SearchItem>, pub total: usize, } /// Search code nodes using semantic search #[utoipa::path( get, path = "/v1/search", tag = "v1", params(SearchRequest), responses( (status = 200, description = "Search results", body = SearchResponse), (status = 400, description = "Invalid input"), (status = 500, description = "Internal error") ) )] pub async fn get_search( State(state): State<AppState>, Query(params): Query<SearchRequest>, ) -> ApiResult<(HeaderMap, Json<SearchResponse>)> { let q = params.q.trim(); if q.is_empty() { return Err(ApiError::Validation("q must not be empty".into())); } let mut limit = params.limit.unwrap_or(10); if limit == 0 || limit > 100 { limit = 10; } let results = state .semantic_search .search_by_text(q, limit) .await .map_err(ApiError::CodeGraph)?; let mut items = Vec::with_capacity(results.len()); let graph = state.graph.read().await; for r in results { if let Ok(Some(node)) = graph.get_node(r.node_id).await { items.push(SearchItem { node_id: r.node_id.to_string(), score: r.score, name: node.name.to_string(), node_type: format!("{:?}", node.node_type), language: format!("{:?}", node.language), file_path: node.location.file_path, }); } } let body = SearchResponse { total: items.len(), results: items, }; Ok((cache_headers(&body), Json(body))) } // -------- Get Node -------- #[derive(Serialize, ToSchema)] pub struct NodeResponse { pub id: String, pub name: String, pub node_type: String, pub language: String, pub location: LocationDto, pub content: Option<String>, pub has_embedding: bool, } /// Get a code node by ID #[utoipa::path( get, path = "/v1/node/{id}", tag = "v1", params( ("id" = String, Path, description = "Node UUID") ), responses( (status = 200, description = "Node details", body = NodeResponse), (status = 404, description = "Node not found"), (status = 400, description = "Invalid ID format") ) )] pub async fn get_node( State(state): State<AppState>, Path(id): Path<String>, ) -> ApiResult<(HeaderMap, Json<NodeResponse>)> { let uuid = Uuid::parse_str(&id) .map_err(|_| ApiError::BadRequest("Invalid node ID format".to_string()))?; let graph = state.graph.read().await; let node = graph .get_node(uuid) .await .map_err(ApiError::CodeGraph)? .ok_or_else(|| ApiError::NotFound(format!("Node {} not found", id)))?; let body = NodeResponse { id: node.id.to_string(), name: node.name.to_string(), node_type: format!("{:?}", node.node_type), language: format!("{:?}", node.language), location: LocationDto { file_path: node.location.file_path, line: node.location.line, column: node.location.column, end_line: node.location.end_line, end_column: node.location.end_column, }, content: node.content.map(|s| s.to_string()), has_embedding: node.embedding.is_some(), }; Ok((cache_headers(&body), Json(body))) } // -------- Neighbors -------- #[derive(Deserialize, IntoParams, ToSchema)] pub struct NeighborsRequest { /// Center node UUID pub id: String, /// Max neighbors to return (1..=500) pub limit: Option<usize>, } #[derive(Serialize, ToSchema)] pub struct NeighborItem { pub id: String, pub name: String, pub node_type: String, pub language: String, } #[derive(Serialize, ToSchema)] pub struct NeighborsResponse { pub center: String, pub total: usize, pub neighbors: Vec<NeighborItem>, } /// Get outgoing neighbors for a node #[utoipa::path( get, path = "/v1/graph/neighbors", tag = "v1", params(NeighborsRequest), responses( (status = 200, description = "Neighbor list", body = NeighborsResponse), (status = 400, description = "Invalid input"), (status = 404, description = "Node not found") ) )] pub async fn get_neighbors( State(state): State<AppState>, Query(params): Query<NeighborsRequest>, ) -> ApiResult<(HeaderMap, Json<NeighborsResponse>)> { let uuid = Uuid::parse_str(params.id.trim()) .map_err(|_| ApiError::BadRequest("Invalid node ID format".to_string()))?; let mut limit = params.limit.unwrap_or(50); if limit == 0 || limit > 500 { limit = 50; } let graph = state.graph.read().await; // Validate node exists let exists = graph .get_node(uuid) .await .map_err(ApiError::CodeGraph)? .is_some(); if !exists { return Err(ApiError::NotFound(format!("Node {} not found", params.id))); } let neighbors = graph .get_neighbors(uuid) .await .map_err(ApiError::CodeGraph)?; let mut out = Vec::new(); for nid in neighbors.into_iter().take(limit) { if let Ok(Some(n)) = graph.get_node(nid).await { out.push(NeighborItem { id: n.id.to_string(), name: n.name.to_string(), node_type: format!("{:?}", n.node_type), language: format!("{:?}", n.language), }); } } let body = NeighborsResponse { center: params.id, total: out.len(), neighbors: out, }; Ok((cache_headers(&body), Json(body))) } fn cache_headers<T: serde::Serialize>(value: &T) -> HeaderMap { let bytes = serde_json::to_vec(value).unwrap_or_default(); let hash = Sha256::digest(&bytes); let etag = format!("\"{:x}\"", hash); let mut headers = HeaderMap::new(); headers.insert( CACHE_CONTROL, HeaderValue::from_static("public, max-age=60"), ); if let Ok(val) = HeaderValue::from_str(&etag) { headers.insert(ETAG, val); } headers }

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