1use crate::store::{IndexStore, SymbolRecord};
7use anyhow::{bail, Result};
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10use std::io::{self, BufRead, Write};
11use std::path::{Path, PathBuf};
12
13const PROTOCOL_VERSION: &str = "2024-11-05";
15
16const SERVER_NAME: &str = "gabb";
18const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
19
20#[derive(Debug, Deserialize)]
23#[allow(dead_code)]
24struct JsonRpcRequest {
25 jsonrpc: String,
26 id: Option<Value>,
27 method: String,
28 #[serde(default)]
29 params: Value,
30}
31
32#[derive(Debug, Serialize)]
33struct JsonRpcResponse {
34 jsonrpc: String,
35 id: Value,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 result: Option<Value>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 error: Option<JsonRpcError>,
40}
41
42#[derive(Debug, Serialize)]
43struct JsonRpcError {
44 code: i32,
45 message: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 data: Option<Value>,
48}
49
50impl JsonRpcResponse {
51 fn success(id: Value, result: Value) -> Self {
52 Self {
53 jsonrpc: "2.0".to_string(),
54 id,
55 result: Some(result),
56 error: None,
57 }
58 }
59
60 fn error(id: Value, code: i32, message: impl Into<String>) -> Self {
61 Self {
62 jsonrpc: "2.0".to_string(),
63 id,
64 result: None,
65 error: Some(JsonRpcError {
66 code,
67 message: message.into(),
68 data: None,
69 }),
70 }
71 }
72}
73
74const PARSE_ERROR: i32 = -32700;
76const INTERNAL_ERROR: i32 = -32603;
77
78#[derive(Debug, Serialize)]
81struct Tool {
82 name: String,
83 description: String,
84 #[serde(rename = "inputSchema")]
85 input_schema: Value,
86}
87
88#[derive(Debug, Serialize)]
89struct ToolResult {
90 content: Vec<ToolContent>,
91 #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
92 is_error: Option<bool>,
93}
94
95#[derive(Debug, Serialize)]
96struct ToolContent {
97 #[serde(rename = "type")]
98 content_type: String,
99 text: String,
100}
101
102impl ToolResult {
103 fn text(text: impl Into<String>) -> Self {
104 Self {
105 content: vec![ToolContent {
106 content_type: "text".to_string(),
107 text: text.into(),
108 }],
109 is_error: None,
110 }
111 }
112
113 fn error(message: impl Into<String>) -> Self {
114 Self {
115 content: vec![ToolContent {
116 content_type: "text".to_string(),
117 text: message.into(),
118 }],
119 is_error: Some(true),
120 }
121 }
122}
123
124pub struct McpServer {
128 workspace_root: PathBuf,
129 db_path: PathBuf,
130 store: Option<IndexStore>,
131 initialized: bool,
132}
133
134impl McpServer {
135 pub fn new(workspace_root: PathBuf, db_path: PathBuf) -> Self {
136 Self {
137 workspace_root,
138 db_path,
139 store: None,
140 initialized: false,
141 }
142 }
143
144 pub fn run(&mut self) -> Result<()> {
146 let stdin = io::stdin();
147 let mut stdout = io::stdout();
148
149 for line in stdin.lock().lines() {
150 let line = line?;
151 if line.is_empty() {
152 continue;
153 }
154
155 let response = self.handle_message(&line);
156 if let Some(response) = response {
157 let json = serde_json::to_string(&response)?;
158 writeln!(stdout, "{}", json)?;
159 stdout.flush()?;
160 }
161 }
162
163 Ok(())
164 }
165
166 fn handle_message(&mut self, line: &str) -> Option<JsonRpcResponse> {
167 let request: JsonRpcRequest = match serde_json::from_str(line) {
169 Ok(req) => req,
170 Err(e) => {
171 return Some(JsonRpcResponse::error(
172 Value::Null,
173 PARSE_ERROR,
174 format!("Parse error: {}", e),
175 ));
176 }
177 };
178
179 let id = match request.id {
181 Some(id) => id,
182 None => {
183 self.handle_notification(&request.method, &request.params);
185 return None;
186 }
187 };
188
189 match self.handle_request(&request.method, &request.params) {
191 Ok(result) => Some(JsonRpcResponse::success(id, result)),
192 Err(e) => Some(JsonRpcResponse::error(id, INTERNAL_ERROR, e.to_string())),
193 }
194 }
195
196 fn handle_notification(&mut self, method: &str, _params: &Value) {
197 match method {
198 "notifications/initialized" => {
199 self.initialized = true;
200 log::info!("MCP client initialized");
201 }
202 _ => {
203 log::debug!("Unknown notification: {}", method);
204 }
205 }
206 }
207
208 fn handle_request(&mut self, method: &str, params: &Value) -> Result<Value> {
209 match method {
210 "initialize" => self.handle_initialize(params),
211 "tools/list" => self.handle_tools_list(),
212 "tools/call" => self.handle_tools_call(params),
213 _ => bail!("Method not found: {}", method),
214 }
215 }
216
217 fn handle_initialize(&mut self, _params: &Value) -> Result<Value> {
218 self.ensure_index()?;
220
221 Ok(json!({
222 "protocolVersion": PROTOCOL_VERSION,
223 "capabilities": {
224 "tools": {
225 "listChanged": false
226 }
227 },
228 "serverInfo": {
229 "name": SERVER_NAME,
230 "version": SERVER_VERSION
231 }
232 }))
233 }
234
235 fn handle_tools_list(&self) -> Result<Value> {
236 let tools = vec![
237 Tool {
238 name: "gabb_symbols".to_string(),
239 description: concat!(
240 "Search for code symbols (functions, classes, interfaces, types, structs, enums, traits) in the indexed codebase. ",
241 "USE THIS INSTEAD OF grep/ripgrep when: finding where a function or class is defined, ",
242 "exploring what methods/functions exist, listing symbols in a file, or searching by symbol kind. ",
243 "Returns precise file:line:column locations. Faster and more accurate than text search for code navigation. ",
244 "Supports TypeScript, Rust, and Kotlin."
245 ).to_string(),
246 input_schema: json!({
247 "type": "object",
248 "properties": {
249 "name": {
250 "type": "string",
251 "description": "Filter by symbol name (exact match). Use this when you know the name you're looking for."
252 },
253 "kind": {
254 "type": "string",
255 "description": "Filter by symbol kind: function, class, interface, type, struct, enum, trait, method, const, variable"
256 },
257 "file": {
258 "type": "string",
259 "description": "Filter to symbols in this file path. Use to explore a specific file's structure."
260 },
261 "limit": {
262 "type": "integer",
263 "description": "Maximum number of results (default: 50). Increase for comprehensive searches."
264 }
265 }
266 }),
267 },
268 Tool {
269 name: "gabb_symbol".to_string(),
270 description: concat!(
271 "Get detailed information about a symbol when you know its name. ",
272 "USE THIS when you have a specific symbol name and want to find where it's defined. ",
273 "Returns the symbol's location, kind, visibility, and container. ",
274 "For exploring unknown code, use gabb_symbols instead."
275 ).to_string(),
276 input_schema: json!({
277 "type": "object",
278 "properties": {
279 "name": {
280 "type": "string",
281 "description": "The exact symbol name to look up (e.g., 'MyClass', 'process_data', 'UserService')"
282 },
283 "kind": {
284 "type": "string",
285 "description": "Optionally filter by kind if the name is ambiguous (function, class, interface, etc.)"
286 }
287 },
288 "required": ["name"]
289 }),
290 },
291 Tool {
292 name: "gabb_definition".to_string(),
293 description: concat!(
294 "Jump from a symbol usage to its definition/declaration. ",
295 "USE THIS when you see a function call, type reference, or variable and want to see where it's defined. ",
296 "Works across files and through imports. Provide the file and position where the symbol is USED, ",
297 "and this returns where it's DEFINED. Essential for understanding unfamiliar code."
298 ).to_string(),
299 input_schema: json!({
300 "type": "object",
301 "properties": {
302 "file": {
303 "type": "string",
304 "description": "Path to the file containing the symbol usage (absolute or relative to workspace)"
305 },
306 "line": {
307 "type": "integer",
308 "description": "1-based line number where the symbol appears"
309 },
310 "character": {
311 "type": "integer",
312 "description": "1-based column number (position within the line)"
313 }
314 },
315 "required": ["file", "line", "character"]
316 }),
317 },
318 Tool {
319 name: "gabb_usages".to_string(),
320 description: concat!(
321 "Find ALL places where a symbol is used/referenced across the codebase. ",
322 "USE THIS BEFORE REFACTORING to understand impact, when investigating how a function is called, ",
323 "or to find all consumers of an API. More accurate than text search - understands code structure ",
324 "and won't match comments or strings. Point to a symbol definition to find all its usages."
325 ).to_string(),
326 input_schema: json!({
327 "type": "object",
328 "properties": {
329 "file": {
330 "type": "string",
331 "description": "Path to the file containing the symbol definition"
332 },
333 "line": {
334 "type": "integer",
335 "description": "1-based line number of the symbol"
336 },
337 "character": {
338 "type": "integer",
339 "description": "1-based column number within the line"
340 },
341 "limit": {
342 "type": "integer",
343 "description": "Maximum usages to return (default: 50). Increase for thorough analysis."
344 }
345 },
346 "required": ["file", "line", "character"]
347 }),
348 },
349 Tool {
350 name: "gabb_implementations".to_string(),
351 description: concat!(
352 "Find all implementations of an interface, trait, or abstract class. ",
353 "USE THIS when you have an interface/trait and want to find concrete implementations, ",
354 "or when exploring a codebase's architecture to understand what classes implement a contract. ",
355 "Point to the interface/trait definition to find all implementing classes/structs."
356 ).to_string(),
357 input_schema: json!({
358 "type": "object",
359 "properties": {
360 "file": {
361 "type": "string",
362 "description": "Path to the file containing the interface/trait definition"
363 },
364 "line": {
365 "type": "integer",
366 "description": "1-based line number of the interface/trait"
367 },
368 "character": {
369 "type": "integer",
370 "description": "1-based column number within the line"
371 },
372 "limit": {
373 "type": "integer",
374 "description": "Maximum implementations to return (default: 50)"
375 }
376 },
377 "required": ["file", "line", "character"]
378 }),
379 },
380 Tool {
381 name: "gabb_daemon_status".to_string(),
382 description: concat!(
383 "Check if the gabb indexing daemon is running and get workspace info. ",
384 "USE THIS to diagnose issues if other gabb tools aren't working, ",
385 "or to verify the index is up-to-date. Returns daemon PID, version, and index location."
386 ).to_string(),
387 input_schema: json!({
388 "type": "object",
389 "properties": {}
390 }),
391 },
392 ];
393
394 Ok(json!({ "tools": tools }))
395 }
396
397 fn handle_tools_call(&mut self, params: &Value) -> Result<Value> {
398 let name = params
399 .get("name")
400 .and_then(|v| v.as_str())
401 .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?;
402
403 let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
404
405 let result = match name {
406 "gabb_symbols" => self.tool_symbols(&arguments),
407 "gabb_symbol" => self.tool_symbol(&arguments),
408 "gabb_definition" => self.tool_definition(&arguments),
409 "gabb_usages" => self.tool_usages(&arguments),
410 "gabb_implementations" => self.tool_implementations(&arguments),
411 "gabb_daemon_status" => self.tool_daemon_status(),
412 _ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
413 }?;
414
415 Ok(serde_json::to_value(result)?)
416 }
417
418 fn ensure_index(&mut self) -> Result<()> {
421 if self.store.is_some() {
422 return Ok(());
423 }
424
425 use crate::daemon;
427
428 if !self.db_path.exists() {
429 log::info!("Index not found. Starting daemon...");
431 daemon::start(&self.workspace_root, &self.db_path, false, true, None)?;
432
433 let max_wait = std::time::Duration::from_secs(60);
435 let start = std::time::Instant::now();
436 while !self.db_path.exists() && start.elapsed() < max_wait {
437 std::thread::sleep(std::time::Duration::from_millis(500));
438 }
439
440 if !self.db_path.exists() {
441 bail!("Daemon started but index not created within 60 seconds");
442 }
443 }
444
445 self.store = Some(IndexStore::open(&self.db_path)?);
447 Ok(())
448 }
449
450 fn get_store(&mut self) -> Result<&IndexStore> {
451 self.ensure_index()?;
452 self.store
453 .as_ref()
454 .ok_or_else(|| anyhow::anyhow!("Store not initialized"))
455 }
456
457 fn tool_symbols(&mut self, args: &Value) -> Result<ToolResult> {
458 let store = self.get_store()?;
459
460 let name = args.get("name").and_then(|v| v.as_str());
461 let kind = args.get("kind").and_then(|v| v.as_str());
462 let file = args.get("file").and_then(|v| v.as_str());
463 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
464
465 let symbols = store.list_symbols(file, kind, name, Some(limit))?;
466
467 if symbols.is_empty() {
468 return Ok(ToolResult::text("No symbols found matching the criteria."));
469 }
470
471 let output = format_symbols(&symbols, &self.workspace_root);
472 Ok(ToolResult::text(output))
473 }
474
475 fn tool_symbol(&mut self, args: &Value) -> Result<ToolResult> {
476 let store = self.get_store()?;
477
478 let name = args
479 .get("name")
480 .and_then(|v| v.as_str())
481 .ok_or_else(|| anyhow::anyhow!("Missing 'name' argument"))?;
482
483 let kind = args.get("kind").and_then(|v| v.as_str());
484
485 let symbols = store.list_symbols(None, kind, Some(name), Some(10))?;
486
487 if symbols.is_empty() {
488 return Ok(ToolResult::text(format!(
489 "No symbol found with name '{}'",
490 name
491 )));
492 }
493
494 let output = format_symbols(&symbols, &self.workspace_root);
495 Ok(ToolResult::text(output))
496 }
497
498 fn tool_definition(&mut self, args: &Value) -> Result<ToolResult> {
499 let file = args
500 .get("file")
501 .and_then(|v| v.as_str())
502 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
503 let line = args
504 .get("line")
505 .and_then(|v| v.as_u64())
506 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
507 let character = args
508 .get("character")
509 .and_then(|v| v.as_u64())
510 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
511 as usize;
512
513 let file_path = self.resolve_path(file);
514
515 if let Some(symbol) = self.find_symbol_at(&file_path, line, character)? {
517 let output = format_symbol(&symbol, &self.workspace_root);
518 return Ok(ToolResult::text(format!("Definition:\n{}", output)));
519 }
520
521 Ok(ToolResult::text(format!(
522 "No symbol found at {}:{}:{}",
523 file, line, character
524 )))
525 }
526
527 fn tool_usages(&mut self, args: &Value) -> Result<ToolResult> {
528 let file = args
529 .get("file")
530 .and_then(|v| v.as_str())
531 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
532 let line = args
533 .get("line")
534 .and_then(|v| v.as_u64())
535 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
536 let character = args
537 .get("character")
538 .and_then(|v| v.as_u64())
539 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
540 as usize;
541 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
542
543 let file_path = self.resolve_path(file);
544
545 let symbol = match self.find_symbol_at(&file_path, line, character)? {
547 Some(s) => s,
548 None => {
549 return Ok(ToolResult::text(format!(
550 "No symbol found at {}:{}:{}",
551 file, line, character
552 )));
553 }
554 };
555
556 let store = self.get_store()?;
558 let refs = store.references_for_symbol(&symbol.id)?;
559
560 if refs.is_empty() {
561 return Ok(ToolResult::text(format!(
562 "No usages found for '{}'",
563 symbol.name
564 )));
565 }
566
567 let refs: Vec<_> = refs.into_iter().take(limit).collect();
568 let mut output = format!("Usages of '{}' ({} found):\n\n", symbol.name, refs.len());
569 for r in &refs {
570 let rel_path = self.relative_path(&r.file);
571 if let Ok((ref_line, ref_col)) = offset_to_line_col(&r.file, r.start as usize) {
573 output.push_str(&format!(" {}:{}:{}\n", rel_path, ref_line, ref_col));
574 }
575 }
576
577 Ok(ToolResult::text(output))
578 }
579
580 fn tool_implementations(&mut self, args: &Value) -> Result<ToolResult> {
581 let file = args
582 .get("file")
583 .and_then(|v| v.as_str())
584 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
585 let line = args
586 .get("line")
587 .and_then(|v| v.as_u64())
588 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
589 let character = args
590 .get("character")
591 .and_then(|v| v.as_u64())
592 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
593 as usize;
594 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
595
596 let file_path = self.resolve_path(file);
597
598 let symbol = match self.find_symbol_at(&file_path, line, character)? {
600 Some(s) => s,
601 None => {
602 return Ok(ToolResult::text(format!(
603 "No symbol found at {}:{}:{}",
604 file, line, character
605 )));
606 }
607 };
608
609 let store = self.get_store()?;
611 let edges = store.edges_to(&symbol.id)?;
612 let impl_ids: Vec<String> = edges.into_iter().map(|e| e.src).collect();
613 let mut impls = store.symbols_by_ids(&impl_ids)?;
614
615 if impls.is_empty() {
616 let fallback = store.list_symbols(None, None, Some(&symbol.name), Some(limit))?;
618 if fallback.len() <= 1 {
619 return Ok(ToolResult::text(format!(
620 "No implementations found for '{}'",
621 symbol.name
622 )));
623 }
624 let output = format_symbols(&fallback, &self.workspace_root);
625 return Ok(ToolResult::text(format!(
626 "Implementations of '{}' (by name):\n\n{}",
627 symbol.name, output
628 )));
629 }
630
631 impls.truncate(limit);
632 let output = format_symbols(&impls, &self.workspace_root);
633 Ok(ToolResult::text(format!(
634 "Implementations of '{}':\n\n{}",
635 symbol.name, output
636 )))
637 }
638
639 fn tool_daemon_status(&mut self) -> Result<ToolResult> {
640 use crate::daemon;
641
642 let status = if let Ok(Some(pid_info)) = daemon::read_pid_file(&self.workspace_root) {
643 if daemon::is_process_running(pid_info.pid) {
644 format!(
645 "Daemon: running (PID {})\nVersion: {}\nWorkspace: {}\nDatabase: {}",
646 pid_info.pid,
647 pid_info.version,
648 self.workspace_root.display(),
649 self.db_path.display()
650 )
651 } else {
652 format!(
653 "Daemon: not running (stale PID file)\nWorkspace: {}\nDatabase: {}",
654 self.workspace_root.display(),
655 self.db_path.display()
656 )
657 }
658 } else {
659 format!(
660 "Daemon: not running\nWorkspace: {}\nDatabase: {}",
661 self.workspace_root.display(),
662 self.db_path.display()
663 )
664 };
665
666 Ok(ToolResult::text(status))
667 }
668
669 fn resolve_path(&self, path: &str) -> PathBuf {
672 let p = PathBuf::from(path);
673 if p.is_absolute() {
674 p
675 } else {
676 self.workspace_root.join(p)
677 }
678 }
679
680 fn relative_path(&self, path: &str) -> String {
681 let p = PathBuf::from(path);
682 p.strip_prefix(&self.workspace_root)
683 .map(|p| p.to_string_lossy().to_string())
684 .unwrap_or_else(|_| path.to_string())
685 }
686
687 fn find_symbol_at(
688 &mut self,
689 file: &Path,
690 line: usize,
691 character: usize,
692 ) -> Result<Option<SymbolRecord>> {
693 let store = self.get_store()?;
694 let file_str = file.to_string_lossy().to_string();
695
696 let content = std::fs::read_to_string(file)?;
698 let mut offset: i64 = 0;
699 for (i, l) in content.lines().enumerate() {
700 if i + 1 == line {
701 offset += character.saturating_sub(1) as i64;
702 break;
703 }
704 offset += l.len() as i64 + 1; }
706
707 let symbols = store.list_symbols(Some(&file_str), None, None, None)?;
709
710 let mut best: Option<SymbolRecord> = None;
712 for sym in symbols {
713 if sym.start <= offset && offset < sym.end {
714 let span = sym.end - sym.start;
715 if best
716 .as_ref()
717 .map(|b| span < (b.end - b.start))
718 .unwrap_or(true)
719 {
720 best = Some(sym);
721 }
722 }
723 }
724
725 Ok(best)
726 }
727}
728
729fn format_symbols(symbols: &[SymbolRecord], workspace_root: &Path) -> String {
732 let mut output = String::new();
733 for sym in symbols {
734 output.push_str(&format_symbol(sym, workspace_root));
735 output.push('\n');
736 }
737 output
738}
739
740fn format_symbol(sym: &SymbolRecord, workspace_root: &Path) -> String {
741 let rel_path = PathBuf::from(&sym.file)
742 .strip_prefix(workspace_root)
743 .map(|p| p.to_string_lossy().to_string())
744 .unwrap_or_else(|_| sym.file.clone());
745
746 let location = if let Ok((line, col)) = offset_to_line_col(&sym.file, sym.start as usize) {
748 format!("{}:{}:{}", rel_path, line, col)
749 } else {
750 format!("{}:offset:{}", rel_path, sym.start)
751 };
752
753 let mut parts = vec![format!("{:<10} {:<30} {}", sym.kind, sym.name, location)];
754
755 if let Some(ref vis) = sym.visibility {
756 parts.push(format!(" visibility: {}", vis));
757 }
758 if let Some(ref container) = sym.container {
759 parts.push(format!(" container: {}", container));
760 }
761
762 parts.join("\n")
763}
764
765fn offset_to_line_col(file_path: &str, offset: usize) -> Result<(usize, usize)> {
767 let content = std::fs::read(file_path)?;
768 let mut line = 1;
769 let mut col = 1;
770 for (i, &b) in content.iter().enumerate() {
771 if i == offset {
772 return Ok((line, col));
773 }
774 if b == b'\n' {
775 line += 1;
776 col = 1;
777 } else {
778 col += 1;
779 }
780 }
781 if offset == content.len() {
782 Ok((line, col))
783 } else {
784 anyhow::bail!("offset out of bounds")
785 }
786}
787
788pub fn run_server(workspace_root: &Path, db_path: &Path) -> Result<()> {
790 let mut server = McpServer::new(workspace_root.to_path_buf(), db_path.to_path_buf());
791 server.run()
792}