1use crate::store::{IndexStore, SymbolRecord};
11use anyhow::{bail, Result};
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15use std::io::{self, BufRead, Write};
16use std::path::{Path, PathBuf};
17
18const WORKSPACE_MARKERS: &[&str] = &[
20 ".git",
21 ".gabb",
22 "Cargo.toml",
23 "package.json",
24 "go.mod",
25 "settings.gradle",
26 "settings.gradle.kts",
27 "pyproject.toml",
28 "pom.xml",
29 "build.gradle",
30 "build.gradle.kts",
31];
32
33const WORKSPACE_DIR_MARKERS: &[&str] = &["gradle", ".git"];
35
36const MAX_CACHED_WORKSPACES: usize = 5;
38
39const PROTOCOL_VERSION: &str = "2024-11-05";
41
42const SERVER_NAME: &str = "gabb";
44const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
45
46#[derive(Debug, Deserialize)]
49#[allow(dead_code)]
50struct JsonRpcRequest {
51 jsonrpc: String,
52 id: Option<Value>,
53 method: String,
54 #[serde(default)]
55 params: Value,
56}
57
58#[derive(Debug, Serialize)]
59struct JsonRpcResponse {
60 jsonrpc: String,
61 id: Value,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 result: Option<Value>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 error: Option<JsonRpcError>,
66}
67
68#[derive(Debug, Serialize)]
69struct JsonRpcError {
70 code: i32,
71 message: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 data: Option<Value>,
74}
75
76impl JsonRpcResponse {
77 fn success(id: Value, result: Value) -> Self {
78 Self {
79 jsonrpc: "2.0".to_string(),
80 id,
81 result: Some(result),
82 error: None,
83 }
84 }
85
86 fn error(id: Value, code: i32, message: impl Into<String>) -> Self {
87 Self {
88 jsonrpc: "2.0".to_string(),
89 id,
90 result: None,
91 error: Some(JsonRpcError {
92 code,
93 message: message.into(),
94 data: None,
95 }),
96 }
97 }
98}
99
100const PARSE_ERROR: i32 = -32700;
102const INTERNAL_ERROR: i32 = -32603;
103
104#[derive(Debug, Serialize)]
107struct Tool {
108 name: String,
109 description: String,
110 #[serde(rename = "inputSchema")]
111 input_schema: Value,
112}
113
114#[derive(Debug, Serialize)]
115struct ToolResult {
116 content: Vec<ToolContent>,
117 #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
118 is_error: Option<bool>,
119}
120
121#[derive(Debug, Serialize)]
122struct ToolContent {
123 #[serde(rename = "type")]
124 content_type: String,
125 text: String,
126}
127
128impl ToolResult {
129 fn text(text: impl Into<String>) -> Self {
130 Self {
131 content: vec![ToolContent {
132 content_type: "text".to_string(),
133 text: text.into(),
134 }],
135 is_error: None,
136 }
137 }
138
139 fn error(message: impl Into<String>) -> Self {
140 Self {
141 content: vec![ToolContent {
142 content_type: "text".to_string(),
143 text: message.into(),
144 }],
145 is_error: Some(true),
146 }
147 }
148}
149
150struct WorkspaceInfo {
154 root: PathBuf,
155 db_path: PathBuf,
156 store: Option<IndexStore>,
157 last_used: std::time::Instant,
158}
159
160pub struct McpServer {
162 default_workspace: PathBuf,
164 default_db_path: PathBuf,
166 workspace_cache: HashMap<PathBuf, WorkspaceInfo>,
168 initialized: bool,
170}
171
172impl McpServer {
173 pub fn new(workspace_root: PathBuf, db_path: PathBuf) -> Self {
174 Self {
175 default_workspace: workspace_root,
176 default_db_path: db_path,
177 workspace_cache: HashMap::new(),
178 initialized: false,
179 }
180 }
181
182 pub fn run(&mut self) -> Result<()> {
184 let stdin = io::stdin();
185 let mut stdout = io::stdout();
186
187 for line in stdin.lock().lines() {
188 let line = line?;
189 if line.is_empty() {
190 continue;
191 }
192
193 let response = self.handle_message(&line);
194 if let Some(response) = response {
195 let json = serde_json::to_string(&response)?;
196 writeln!(stdout, "{}", json)?;
197 stdout.flush()?;
198 }
199 }
200
201 Ok(())
202 }
203
204 fn handle_message(&mut self, line: &str) -> Option<JsonRpcResponse> {
205 let request: JsonRpcRequest = match serde_json::from_str(line) {
207 Ok(req) => req,
208 Err(e) => {
209 return Some(JsonRpcResponse::error(
210 Value::Null,
211 PARSE_ERROR,
212 format!("Parse error: {}", e),
213 ));
214 }
215 };
216
217 let id = match request.id {
219 Some(id) => id,
220 None => {
221 self.handle_notification(&request.method, &request.params);
223 return None;
224 }
225 };
226
227 match self.handle_request(&request.method, &request.params) {
229 Ok(result) => Some(JsonRpcResponse::success(id, result)),
230 Err(e) => Some(JsonRpcResponse::error(id, INTERNAL_ERROR, e.to_string())),
231 }
232 }
233
234 fn handle_notification(&mut self, method: &str, _params: &Value) {
235 match method {
236 "notifications/initialized" => {
237 self.initialized = true;
238 log::info!("MCP client initialized");
239 }
240 _ => {
241 log::debug!("Unknown notification: {}", method);
242 }
243 }
244 }
245
246 fn handle_request(&mut self, method: &str, params: &Value) -> Result<Value> {
247 match method {
248 "initialize" => self.handle_initialize(params),
249 "tools/list" => self.handle_tools_list(),
250 "tools/call" => self.handle_tools_call(params),
251 _ => bail!("Method not found: {}", method),
252 }
253 }
254
255 fn handle_initialize(&mut self, _params: &Value) -> Result<Value> {
256 let default_workspace = self.default_workspace.clone();
258 self.ensure_workspace_index(&default_workspace)?;
259
260 Ok(json!({
261 "protocolVersion": PROTOCOL_VERSION,
262 "capabilities": {
263 "tools": {
264 "listChanged": false
265 }
266 },
267 "serverInfo": {
268 "name": SERVER_NAME,
269 "version": SERVER_VERSION
270 }
271 }))
272 }
273
274 fn handle_tools_list(&self) -> Result<Value> {
275 let tools = vec![
276 Tool {
277 name: "gabb_symbols".to_string(),
278 description: concat!(
279 "Search for code symbols (functions, classes, interfaces, types, structs, enums, traits) in the indexed codebase. ",
280 "USE THIS INSTEAD OF grep/ripgrep when: finding where a function or class is defined, ",
281 "exploring what methods/functions exist, listing symbols in a file, or searching by symbol kind. ",
282 "Returns precise file:line:column locations. Faster and more accurate than text search for code navigation. ",
283 "Supports TypeScript, Rust, and Kotlin."
284 ).to_string(),
285 input_schema: json!({
286 "type": "object",
287 "properties": {
288 "name": {
289 "type": "string",
290 "description": "Filter by symbol name (exact match). Use this when you know the name you're looking for."
291 },
292 "kind": {
293 "type": "string",
294 "description": "Filter by symbol kind: function, class, interface, type, struct, enum, trait, method, const, variable"
295 },
296 "file": {
297 "type": "string",
298 "description": "Filter to symbols in this file path. Use to explore a specific file's structure."
299 },
300 "limit": {
301 "type": "integer",
302 "description": "Maximum number of results (default: 50). Increase for comprehensive searches."
303 }
304 }
305 }),
306 },
307 Tool {
308 name: "gabb_symbol".to_string(),
309 description: concat!(
310 "Get detailed information about a symbol when you know its name. ",
311 "USE THIS when you have a specific symbol name and want to find where it's defined. ",
312 "Returns the symbol's location, kind, visibility, and container. ",
313 "For exploring unknown code, use gabb_symbols instead."
314 ).to_string(),
315 input_schema: json!({
316 "type": "object",
317 "properties": {
318 "name": {
319 "type": "string",
320 "description": "The exact symbol name to look up (e.g., 'MyClass', 'process_data', 'UserService')"
321 },
322 "kind": {
323 "type": "string",
324 "description": "Optionally filter by kind if the name is ambiguous (function, class, interface, etc.)"
325 }
326 },
327 "required": ["name"]
328 }),
329 },
330 Tool {
331 name: "gabb_definition".to_string(),
332 description: concat!(
333 "Jump from a symbol usage to its definition/declaration. ",
334 "USE THIS when you see a function call, type reference, or variable and want to see where it's defined. ",
335 "Works across files and through imports. Provide the file and position where the symbol is USED, ",
336 "and this returns where it's DEFINED. Essential for understanding unfamiliar code."
337 ).to_string(),
338 input_schema: json!({
339 "type": "object",
340 "properties": {
341 "file": {
342 "type": "string",
343 "description": "Path to the file containing the symbol usage (absolute or relative to workspace)"
344 },
345 "line": {
346 "type": "integer",
347 "description": "1-based line number where the symbol appears"
348 },
349 "character": {
350 "type": "integer",
351 "description": "1-based column number (position within the line)"
352 }
353 },
354 "required": ["file", "line", "character"]
355 }),
356 },
357 Tool {
358 name: "gabb_usages".to_string(),
359 description: concat!(
360 "Find ALL places where a symbol is used/referenced across the codebase. ",
361 "USE THIS BEFORE REFACTORING to understand impact, when investigating how a function is called, ",
362 "or to find all consumers of an API. More accurate than text search - understands code structure ",
363 "and won't match comments or strings. Point to a symbol definition to find all its usages."
364 ).to_string(),
365 input_schema: json!({
366 "type": "object",
367 "properties": {
368 "file": {
369 "type": "string",
370 "description": "Path to the file containing the symbol definition"
371 },
372 "line": {
373 "type": "integer",
374 "description": "1-based line number of the symbol"
375 },
376 "character": {
377 "type": "integer",
378 "description": "1-based column number within the line"
379 },
380 "limit": {
381 "type": "integer",
382 "description": "Maximum usages to return (default: 50). Increase for thorough analysis."
383 }
384 },
385 "required": ["file", "line", "character"]
386 }),
387 },
388 Tool {
389 name: "gabb_implementations".to_string(),
390 description: concat!(
391 "Find all implementations of an interface, trait, or abstract class. ",
392 "USE THIS when you have an interface/trait and want to find concrete implementations, ",
393 "or when exploring a codebase's architecture to understand what classes implement a contract. ",
394 "Point to the interface/trait definition to find all implementing classes/structs."
395 ).to_string(),
396 input_schema: json!({
397 "type": "object",
398 "properties": {
399 "file": {
400 "type": "string",
401 "description": "Path to the file containing the interface/trait definition"
402 },
403 "line": {
404 "type": "integer",
405 "description": "1-based line number of the interface/trait"
406 },
407 "character": {
408 "type": "integer",
409 "description": "1-based column number within the line"
410 },
411 "limit": {
412 "type": "integer",
413 "description": "Maximum implementations to return (default: 50)"
414 }
415 },
416 "required": ["file", "line", "character"]
417 }),
418 },
419 Tool {
420 name: "gabb_daemon_status".to_string(),
421 description: concat!(
422 "Check if the gabb indexing daemon is running and get workspace info. ",
423 "USE THIS to diagnose issues if other gabb tools aren't working, ",
424 "or to verify the index is up-to-date. Returns daemon PID, version, and index location."
425 ).to_string(),
426 input_schema: json!({
427 "type": "object",
428 "properties": {}
429 }),
430 },
431 ];
432
433 Ok(json!({ "tools": tools }))
434 }
435
436 fn handle_tools_call(&mut self, params: &Value) -> Result<Value> {
437 let name = params
438 .get("name")
439 .and_then(|v| v.as_str())
440 .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?;
441
442 let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
443
444 let result = match name {
445 "gabb_symbols" => self.tool_symbols(&arguments),
446 "gabb_symbol" => self.tool_symbol(&arguments),
447 "gabb_definition" => self.tool_definition(&arguments),
448 "gabb_usages" => self.tool_usages(&arguments),
449 "gabb_implementations" => self.tool_implementations(&arguments),
450 "gabb_daemon_status" => self.tool_daemon_status(),
451 _ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
452 }?;
453
454 Ok(serde_json::to_value(result)?)
455 }
456
457 fn infer_workspace(&self, file_path: &Path) -> Option<PathBuf> {
461 let file_path = if file_path.is_absolute() {
462 file_path.to_path_buf()
463 } else {
464 self.default_workspace.join(file_path)
465 };
466
467 let mut current = file_path.parent()?;
468
469 loop {
470 for marker in WORKSPACE_MARKERS {
472 if current.join(marker).exists() {
473 return Some(current.to_path_buf());
474 }
475 }
476
477 for marker in WORKSPACE_DIR_MARKERS {
479 let marker_path = current.join(marker);
480 if marker_path.is_dir() {
481 return Some(current.to_path_buf());
482 }
483 }
484
485 current = current.parent()?;
487 }
488 }
489
490 fn get_or_create_workspace(&mut self, workspace_root: &Path) -> Result<&mut WorkspaceInfo> {
492 let workspace_root = workspace_root
493 .canonicalize()
494 .unwrap_or_else(|_| workspace_root.to_path_buf());
495
496 if !self.workspace_cache.contains_key(&workspace_root)
498 && self.workspace_cache.len() >= MAX_CACHED_WORKSPACES
499 {
500 if let Some(oldest_key) = self
502 .workspace_cache
503 .iter()
504 .min_by_key(|(_, info)| info.last_used)
505 .map(|(k, _)| k.clone())
506 {
507 log::debug!("Evicting workspace from cache: {}", oldest_key.display());
508 self.workspace_cache.remove(&oldest_key);
509 }
510 }
511
512 if !self.workspace_cache.contains_key(&workspace_root) {
514 let db_path = workspace_root.join(".gabb/index.db");
515 log::debug!(
516 "Adding workspace to cache: {} (db: {})",
517 workspace_root.display(),
518 db_path.display()
519 );
520 self.workspace_cache.insert(
521 workspace_root.clone(),
522 WorkspaceInfo {
523 root: workspace_root.clone(),
524 db_path,
525 store: None,
526 last_used: std::time::Instant::now(),
527 },
528 );
529 }
530
531 let info = self.workspace_cache.get_mut(&workspace_root).unwrap();
533 info.last_used = std::time::Instant::now();
534 Ok(info)
535 }
536
537 fn ensure_workspace_index(&mut self, workspace_root: &Path) -> Result<()> {
539 use crate::daemon;
540
541 let info = self.get_or_create_workspace(workspace_root)?;
542
543 if info.store.is_some() {
544 return Ok(());
545 }
546
547 if !info.db_path.exists() {
548 log::info!(
550 "Index not found for {}. Starting daemon...",
551 info.root.display()
552 );
553 daemon::start(&info.root, &info.db_path, false, true, None)?;
554
555 let max_wait = std::time::Duration::from_secs(60);
557 let start = std::time::Instant::now();
558 let db_path = info.db_path.clone();
559 while !db_path.exists() && start.elapsed() < max_wait {
560 std::thread::sleep(std::time::Duration::from_millis(500));
561 }
562
563 if !db_path.exists() {
564 bail!("Daemon started but index not created within 60 seconds");
565 }
566 }
567
568 let info = self.workspace_cache.get_mut(workspace_root).unwrap();
570 info.store = Some(IndexStore::open(&info.db_path)?);
571 Ok(())
572 }
573
574 fn workspace_for_file(&self, file_path: Option<&str>) -> PathBuf {
576 if let Some(path) = file_path {
577 let path = PathBuf::from(path);
578 if let Some(workspace) = self.infer_workspace(&path) {
579 return workspace;
580 }
581 }
582 self.default_workspace.clone()
583 }
584
585 fn get_store_for_workspace(&mut self, workspace_root: &Path) -> Result<&IndexStore> {
587 self.ensure_workspace_index(workspace_root)?;
588 let info = self.workspace_cache.get(workspace_root).unwrap();
589 info.store
590 .as_ref()
591 .ok_or_else(|| anyhow::anyhow!("Store not initialized"))
592 }
593
594 fn tool_symbols(&mut self, args: &Value) -> Result<ToolResult> {
597 let name = args.get("name").and_then(|v| v.as_str());
598 let kind = args.get("kind").and_then(|v| v.as_str());
599 let file = args.get("file").and_then(|v| v.as_str());
600 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
601
602 let workspace = self.workspace_for_file(file);
604 let store = self.get_store_for_workspace(&workspace)?;
605
606 let symbols = store.list_symbols(file, kind, name, Some(limit))?;
607
608 if symbols.is_empty() {
609 return Ok(ToolResult::text("No symbols found matching the criteria."));
610 }
611
612 let output = format_symbols(&symbols, &workspace);
613 Ok(ToolResult::text(output))
614 }
615
616 fn tool_symbol(&mut self, args: &Value) -> Result<ToolResult> {
617 let workspace = self.default_workspace.clone();
619 let store = self.get_store_for_workspace(&workspace)?;
620
621 let name = args
622 .get("name")
623 .and_then(|v| v.as_str())
624 .ok_or_else(|| anyhow::anyhow!("Missing 'name' argument"))?;
625
626 let kind = args.get("kind").and_then(|v| v.as_str());
627
628 let symbols = store.list_symbols(None, kind, Some(name), Some(10))?;
629
630 if symbols.is_empty() {
631 return Ok(ToolResult::text(format!(
632 "No symbol found with name '{}'",
633 name
634 )));
635 }
636
637 let output = format_symbols(&symbols, &workspace);
638 Ok(ToolResult::text(output))
639 }
640
641 fn tool_definition(&mut self, args: &Value) -> Result<ToolResult> {
642 let file = args
643 .get("file")
644 .and_then(|v| v.as_str())
645 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
646 let line = args
647 .get("line")
648 .and_then(|v| v.as_u64())
649 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
650 let character = args
651 .get("character")
652 .and_then(|v| v.as_u64())
653 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
654 as usize;
655
656 let workspace = self.workspace_for_file(Some(file));
658 let file_path = self.resolve_path_for_workspace(file, &workspace);
659
660 if let Some(symbol) = self.find_symbol_at_in_workspace(&file_path, line, character, &workspace)? {
662 let output = format_symbol(&symbol, &workspace);
663 return Ok(ToolResult::text(format!("Definition:\n{}", output)));
664 }
665
666 Ok(ToolResult::text(format!(
667 "No symbol found at {}:{}:{}",
668 file, line, character
669 )))
670 }
671
672 fn tool_usages(&mut self, args: &Value) -> Result<ToolResult> {
673 let file = args
674 .get("file")
675 .and_then(|v| v.as_str())
676 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
677 let line = args
678 .get("line")
679 .and_then(|v| v.as_u64())
680 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
681 let character = args
682 .get("character")
683 .and_then(|v| v.as_u64())
684 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
685 as usize;
686 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
687
688 let workspace = self.workspace_for_file(Some(file));
690 let file_path = self.resolve_path_for_workspace(file, &workspace);
691
692 let symbol = match self.find_symbol_at_in_workspace(&file_path, line, character, &workspace)? {
694 Some(s) => s,
695 None => {
696 return Ok(ToolResult::text(format!(
697 "No symbol found at {}:{}:{}",
698 file, line, character
699 )));
700 }
701 };
702
703 let store = self.get_store_for_workspace(&workspace)?;
705 let refs = store.references_for_symbol(&symbol.id)?;
706
707 if refs.is_empty() {
708 return Ok(ToolResult::text(format!(
709 "No usages found for '{}'",
710 symbol.name
711 )));
712 }
713
714 let refs: Vec<_> = refs.into_iter().take(limit).collect();
715 let mut output = format!("Usages of '{}' ({} found):\n\n", symbol.name, refs.len());
716 for r in &refs {
717 let rel_path = relative_path_for_workspace(&r.file, &workspace);
718 if let Ok((ref_line, ref_col)) = offset_to_line_col(&r.file, r.start as usize) {
720 output.push_str(&format!(" {}:{}:{}\n", rel_path, ref_line, ref_col));
721 }
722 }
723
724 Ok(ToolResult::text(output))
725 }
726
727 fn tool_implementations(&mut self, args: &Value) -> Result<ToolResult> {
728 let file = args
729 .get("file")
730 .and_then(|v| v.as_str())
731 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
732 let line = args
733 .get("line")
734 .and_then(|v| v.as_u64())
735 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
736 let character = args
737 .get("character")
738 .and_then(|v| v.as_u64())
739 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
740 as usize;
741 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
742
743 let workspace = self.workspace_for_file(Some(file));
745 let file_path = self.resolve_path_for_workspace(file, &workspace);
746
747 let symbol = match self.find_symbol_at_in_workspace(&file_path, line, character, &workspace)? {
749 Some(s) => s,
750 None => {
751 return Ok(ToolResult::text(format!(
752 "No symbol found at {}:{}:{}",
753 file, line, character
754 )));
755 }
756 };
757
758 let store = self.get_store_for_workspace(&workspace)?;
760 let edges = store.edges_to(&symbol.id)?;
761 let impl_ids: Vec<String> = edges.into_iter().map(|e| e.src).collect();
762 let mut impls = store.symbols_by_ids(&impl_ids)?;
763
764 if impls.is_empty() {
765 let fallback = store.list_symbols(None, None, Some(&symbol.name), Some(limit))?;
767 if fallback.len() <= 1 {
768 return Ok(ToolResult::text(format!(
769 "No implementations found for '{}'",
770 symbol.name
771 )));
772 }
773 let output = format_symbols(&fallback, &workspace);
774 return Ok(ToolResult::text(format!(
775 "Implementations of '{}' (by name):\n\n{}",
776 symbol.name, output
777 )));
778 }
779
780 impls.truncate(limit);
781 let output = format_symbols(&impls, &workspace);
782 Ok(ToolResult::text(format!(
783 "Implementations of '{}':\n\n{}",
784 symbol.name, output
785 )))
786 }
787
788 fn tool_daemon_status(&mut self) -> Result<ToolResult> {
789 use crate::daemon;
790
791 let mut status = String::new();
792
793 status.push_str("Default Workspace:\n");
795 if let Ok(Some(pid_info)) = daemon::read_pid_file(&self.default_workspace) {
796 if daemon::is_process_running(pid_info.pid) {
797 status.push_str(&format!(
798 " Daemon: running (PID {})\n Version: {}\n Root: {}\n Database: {}\n",
799 pid_info.pid,
800 pid_info.version,
801 self.default_workspace.display(),
802 self.default_db_path.display()
803 ));
804 } else {
805 status.push_str(&format!(
806 " Daemon: not running (stale PID file)\n Root: {}\n Database: {}\n",
807 self.default_workspace.display(),
808 self.default_db_path.display()
809 ));
810 }
811 } else {
812 status.push_str(&format!(
813 " Daemon: not running\n Root: {}\n Database: {}\n",
814 self.default_workspace.display(),
815 self.default_db_path.display()
816 ));
817 }
818
819 if !self.workspace_cache.is_empty() {
821 status.push_str(&format!(
822 "\nCached Workspaces ({}/{}):\n",
823 self.workspace_cache.len(),
824 MAX_CACHED_WORKSPACES
825 ));
826 for (root, info) in &self.workspace_cache {
827 let daemon_status = if let Ok(Some(pid_info)) = daemon::read_pid_file(root) {
828 if daemon::is_process_running(pid_info.pid) {
829 format!("running (PID {})", pid_info.pid)
830 } else {
831 "not running".to_string()
832 }
833 } else {
834 "not running".to_string()
835 };
836 let index_status = if info.store.is_some() {
837 "loaded"
838 } else if info.db_path.exists() {
839 "available"
840 } else {
841 "not indexed"
842 };
843 status.push_str(&format!(
844 " {}\n Daemon: {}, Index: {}\n",
845 root.display(),
846 daemon_status,
847 index_status
848 ));
849 }
850 }
851
852 Ok(ToolResult::text(status))
853 }
854
855 fn resolve_path_for_workspace(&self, path: &str, workspace: &Path) -> PathBuf {
858 let p = PathBuf::from(path);
859 if p.is_absolute() {
860 p
861 } else {
862 workspace.join(p)
863 }
864 }
865
866 fn find_symbol_at_in_workspace(
867 &mut self,
868 file: &Path,
869 line: usize,
870 character: usize,
871 workspace: &Path,
872 ) -> Result<Option<SymbolRecord>> {
873 let store = self.get_store_for_workspace(workspace)?;
874 let file_str = file.to_string_lossy().to_string();
875
876 let content = std::fs::read_to_string(file)?;
878 let mut offset: i64 = 0;
879 for (i, l) in content.lines().enumerate() {
880 if i + 1 == line {
881 offset += character.saturating_sub(1) as i64;
882 break;
883 }
884 offset += l.len() as i64 + 1; }
886
887 let symbols = store.list_symbols(Some(&file_str), None, None, None)?;
889
890 let mut best: Option<SymbolRecord> = None;
892 for sym in symbols {
893 if sym.start <= offset && offset < sym.end {
894 let span = sym.end - sym.start;
895 if best
896 .as_ref()
897 .map(|b| span < (b.end - b.start))
898 .unwrap_or(true)
899 {
900 best = Some(sym);
901 }
902 }
903 }
904
905 Ok(best)
906 }
907}
908
909fn relative_path_for_workspace(path: &str, workspace: &Path) -> String {
913 let p = PathBuf::from(path);
914 p.strip_prefix(workspace)
915 .map(|p| p.to_string_lossy().to_string())
916 .unwrap_or_else(|_| path.to_string())
917}
918
919fn format_symbols(symbols: &[SymbolRecord], workspace_root: &Path) -> String {
920 let mut output = String::new();
921 for sym in symbols {
922 output.push_str(&format_symbol(sym, workspace_root));
923 output.push('\n');
924 }
925 output
926}
927
928fn format_symbol(sym: &SymbolRecord, workspace_root: &Path) -> String {
929 let rel_path = PathBuf::from(&sym.file)
930 .strip_prefix(workspace_root)
931 .map(|p| p.to_string_lossy().to_string())
932 .unwrap_or_else(|_| sym.file.clone());
933
934 let location = if let Ok((line, col)) = offset_to_line_col(&sym.file, sym.start as usize) {
936 format!("{}:{}:{}", rel_path, line, col)
937 } else {
938 format!("{}:offset:{}", rel_path, sym.start)
939 };
940
941 let mut parts = vec![format!("{:<10} {:<30} {}", sym.kind, sym.name, location)];
942
943 if let Some(ref vis) = sym.visibility {
944 parts.push(format!(" visibility: {}", vis));
945 }
946 if let Some(ref container) = sym.container {
947 parts.push(format!(" container: {}", container));
948 }
949
950 parts.join("\n")
951}
952
953fn offset_to_line_col(file_path: &str, offset: usize) -> Result<(usize, usize)> {
955 let content = std::fs::read(file_path)?;
956 let mut line = 1;
957 let mut col = 1;
958 for (i, &b) in content.iter().enumerate() {
959 if i == offset {
960 return Ok((line, col));
961 }
962 if b == b'\n' {
963 line += 1;
964 col = 1;
965 } else {
966 col += 1;
967 }
968 }
969 if offset == content.len() {
970 Ok((line, col))
971 } else {
972 anyhow::bail!("offset out of bounds")
973 }
974}
975
976pub fn run_server(workspace_root: &Path, db_path: &Path) -> Result<()> {
978 let mut server = McpServer::new(workspace_root.to_path_buf(), db_path.to_path_buf());
979 server.run()
980}