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: "List or search symbols in the codebase. Returns functions, classes, interfaces, types, etc.".to_string(),
240 input_schema: json!({
241 "type": "object",
242 "properties": {
243 "name": {
244 "type": "string",
245 "description": "Filter by symbol name (exact match)"
246 },
247 "kind": {
248 "type": "string",
249 "description": "Filter by kind (function, class, interface, type, etc.)"
250 },
251 "file": {
252 "type": "string",
253 "description": "Filter by file path"
254 },
255 "limit": {
256 "type": "integer",
257 "description": "Maximum number of results (default: 50)"
258 }
259 }
260 }),
261 },
262 Tool {
263 name: "gabb_symbol".to_string(),
264 description: "Get detailed information about a symbol by name.".to_string(),
265 input_schema: json!({
266 "type": "object",
267 "properties": {
268 "name": {
269 "type": "string",
270 "description": "Symbol name to find"
271 },
272 "kind": {
273 "type": "string",
274 "description": "Filter by kind (function, class, interface, type, etc.)"
275 }
276 },
277 "required": ["name"]
278 }),
279 },
280 Tool {
281 name: "gabb_definition".to_string(),
282 description: "Go to definition for a symbol at a source position.".to_string(),
283 input_schema: json!({
284 "type": "object",
285 "properties": {
286 "file": {
287 "type": "string",
288 "description": "Source file path"
289 },
290 "line": {
291 "type": "integer",
292 "description": "1-based line number"
293 },
294 "character": {
295 "type": "integer",
296 "description": "1-based column number"
297 }
298 },
299 "required": ["file", "line", "character"]
300 }),
301 },
302 Tool {
303 name: "gabb_usages".to_string(),
304 description: "Find all usages/references of a symbol at a source position.".to_string(),
305 input_schema: json!({
306 "type": "object",
307 "properties": {
308 "file": {
309 "type": "string",
310 "description": "Source file path"
311 },
312 "line": {
313 "type": "integer",
314 "description": "1-based line number"
315 },
316 "character": {
317 "type": "integer",
318 "description": "1-based column number"
319 },
320 "limit": {
321 "type": "integer",
322 "description": "Maximum number of results (default: 50)"
323 }
324 },
325 "required": ["file", "line", "character"]
326 }),
327 },
328 Tool {
329 name: "gabb_implementations".to_string(),
330 description: "Find implementations of an interface, trait, or abstract class.".to_string(),
331 input_schema: json!({
332 "type": "object",
333 "properties": {
334 "file": {
335 "type": "string",
336 "description": "Source file path"
337 },
338 "line": {
339 "type": "integer",
340 "description": "1-based line number"
341 },
342 "character": {
343 "type": "integer",
344 "description": "1-based column number"
345 },
346 "limit": {
347 "type": "integer",
348 "description": "Maximum number of results (default: 50)"
349 }
350 },
351 "required": ["file", "line", "character"]
352 }),
353 },
354 Tool {
355 name: "gabb_daemon_status".to_string(),
356 description: "Check the status of the gabb indexing daemon.".to_string(),
357 input_schema: json!({
358 "type": "object",
359 "properties": {}
360 }),
361 },
362 ];
363
364 Ok(json!({ "tools": tools }))
365 }
366
367 fn handle_tools_call(&mut self, params: &Value) -> Result<Value> {
368 let name = params
369 .get("name")
370 .and_then(|v| v.as_str())
371 .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?;
372
373 let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
374
375 let result = match name {
376 "gabb_symbols" => self.tool_symbols(&arguments),
377 "gabb_symbol" => self.tool_symbol(&arguments),
378 "gabb_definition" => self.tool_definition(&arguments),
379 "gabb_usages" => self.tool_usages(&arguments),
380 "gabb_implementations" => self.tool_implementations(&arguments),
381 "gabb_daemon_status" => self.tool_daemon_status(),
382 _ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
383 }?;
384
385 Ok(serde_json::to_value(result)?)
386 }
387
388 fn ensure_index(&mut self) -> Result<()> {
391 if self.store.is_some() {
392 return Ok(());
393 }
394
395 use crate::daemon;
397
398 if !self.db_path.exists() {
399 log::info!("Index not found. Starting daemon...");
401 daemon::start(&self.workspace_root, &self.db_path, false, true, None)?;
402
403 let max_wait = std::time::Duration::from_secs(60);
405 let start = std::time::Instant::now();
406 while !self.db_path.exists() && start.elapsed() < max_wait {
407 std::thread::sleep(std::time::Duration::from_millis(500));
408 }
409
410 if !self.db_path.exists() {
411 bail!("Daemon started but index not created within 60 seconds");
412 }
413 }
414
415 self.store = Some(IndexStore::open(&self.db_path)?);
417 Ok(())
418 }
419
420 fn get_store(&mut self) -> Result<&IndexStore> {
421 self.ensure_index()?;
422 self.store
423 .as_ref()
424 .ok_or_else(|| anyhow::anyhow!("Store not initialized"))
425 }
426
427 fn tool_symbols(&mut self, args: &Value) -> Result<ToolResult> {
428 let store = self.get_store()?;
429
430 let name = args.get("name").and_then(|v| v.as_str());
431 let kind = args.get("kind").and_then(|v| v.as_str());
432 let file = args.get("file").and_then(|v| v.as_str());
433 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
434
435 let symbols = store.list_symbols(file, kind, name, Some(limit))?;
436
437 if symbols.is_empty() {
438 return Ok(ToolResult::text("No symbols found matching the criteria."));
439 }
440
441 let output = format_symbols(&symbols, &self.workspace_root);
442 Ok(ToolResult::text(output))
443 }
444
445 fn tool_symbol(&mut self, args: &Value) -> Result<ToolResult> {
446 let store = self.get_store()?;
447
448 let name = args
449 .get("name")
450 .and_then(|v| v.as_str())
451 .ok_or_else(|| anyhow::anyhow!("Missing 'name' argument"))?;
452
453 let kind = args.get("kind").and_then(|v| v.as_str());
454
455 let symbols = store.list_symbols(None, kind, Some(name), Some(10))?;
456
457 if symbols.is_empty() {
458 return Ok(ToolResult::text(format!(
459 "No symbol found with name '{}'",
460 name
461 )));
462 }
463
464 let output = format_symbols(&symbols, &self.workspace_root);
465 Ok(ToolResult::text(output))
466 }
467
468 fn tool_definition(&mut self, args: &Value) -> Result<ToolResult> {
469 let file = args
470 .get("file")
471 .and_then(|v| v.as_str())
472 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
473 let line = args
474 .get("line")
475 .and_then(|v| v.as_u64())
476 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
477 let character = args
478 .get("character")
479 .and_then(|v| v.as_u64())
480 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
481 as usize;
482
483 let file_path = self.resolve_path(file);
484
485 if let Some(symbol) = self.find_symbol_at(&file_path, line, character)? {
487 let output = format_symbol(&symbol, &self.workspace_root);
488 return Ok(ToolResult::text(format!("Definition:\n{}", output)));
489 }
490
491 Ok(ToolResult::text(format!(
492 "No symbol found at {}:{}:{}",
493 file, line, character
494 )))
495 }
496
497 fn tool_usages(&mut self, args: &Value) -> Result<ToolResult> {
498 let file = args
499 .get("file")
500 .and_then(|v| v.as_str())
501 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
502 let line = args
503 .get("line")
504 .and_then(|v| v.as_u64())
505 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
506 let character = args
507 .get("character")
508 .and_then(|v| v.as_u64())
509 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
510 as usize;
511 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
512
513 let file_path = self.resolve_path(file);
514
515 let symbol = match self.find_symbol_at(&file_path, line, character)? {
517 Some(s) => s,
518 None => {
519 return Ok(ToolResult::text(format!(
520 "No symbol found at {}:{}:{}",
521 file, line, character
522 )));
523 }
524 };
525
526 let store = self.get_store()?;
528 let refs = store.references_for_symbol(&symbol.id)?;
529
530 if refs.is_empty() {
531 return Ok(ToolResult::text(format!(
532 "No usages found for '{}'",
533 symbol.name
534 )));
535 }
536
537 let refs: Vec<_> = refs.into_iter().take(limit).collect();
538 let mut output = format!("Usages of '{}' ({} found):\n\n", symbol.name, refs.len());
539 for r in &refs {
540 let rel_path = self.relative_path(&r.file);
541 if let Ok((ref_line, ref_col)) = offset_to_line_col(&r.file, r.start as usize) {
543 output.push_str(&format!(" {}:{}:{}\n", rel_path, ref_line, ref_col));
544 }
545 }
546
547 Ok(ToolResult::text(output))
548 }
549
550 fn tool_implementations(&mut self, args: &Value) -> Result<ToolResult> {
551 let file = args
552 .get("file")
553 .and_then(|v| v.as_str())
554 .ok_or_else(|| anyhow::anyhow!("Missing 'file' argument"))?;
555 let line = args
556 .get("line")
557 .and_then(|v| v.as_u64())
558 .ok_or_else(|| anyhow::anyhow!("Missing 'line' argument"))? as usize;
559 let character = args
560 .get("character")
561 .and_then(|v| v.as_u64())
562 .ok_or_else(|| anyhow::anyhow!("Missing 'character' argument"))?
563 as usize;
564 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
565
566 let file_path = self.resolve_path(file);
567
568 let symbol = match self.find_symbol_at(&file_path, line, character)? {
570 Some(s) => s,
571 None => {
572 return Ok(ToolResult::text(format!(
573 "No symbol found at {}:{}:{}",
574 file, line, character
575 )));
576 }
577 };
578
579 let store = self.get_store()?;
581 let edges = store.edges_to(&symbol.id)?;
582 let impl_ids: Vec<String> = edges.into_iter().map(|e| e.src).collect();
583 let mut impls = store.symbols_by_ids(&impl_ids)?;
584
585 if impls.is_empty() {
586 let fallback = store.list_symbols(None, None, Some(&symbol.name), Some(limit))?;
588 if fallback.len() <= 1 {
589 return Ok(ToolResult::text(format!(
590 "No implementations found for '{}'",
591 symbol.name
592 )));
593 }
594 let output = format_symbols(&fallback, &self.workspace_root);
595 return Ok(ToolResult::text(format!(
596 "Implementations of '{}' (by name):\n\n{}",
597 symbol.name, output
598 )));
599 }
600
601 impls.truncate(limit);
602 let output = format_symbols(&impls, &self.workspace_root);
603 Ok(ToolResult::text(format!(
604 "Implementations of '{}':\n\n{}",
605 symbol.name, output
606 )))
607 }
608
609 fn tool_daemon_status(&mut self) -> Result<ToolResult> {
610 use crate::daemon;
611
612 let status = if let Ok(Some(pid_info)) = daemon::read_pid_file(&self.workspace_root) {
613 if daemon::is_process_running(pid_info.pid) {
614 format!(
615 "Daemon: running (PID {})\nVersion: {}\nWorkspace: {}\nDatabase: {}",
616 pid_info.pid,
617 pid_info.version,
618 self.workspace_root.display(),
619 self.db_path.display()
620 )
621 } else {
622 format!(
623 "Daemon: not running (stale PID file)\nWorkspace: {}\nDatabase: {}",
624 self.workspace_root.display(),
625 self.db_path.display()
626 )
627 }
628 } else {
629 format!(
630 "Daemon: not running\nWorkspace: {}\nDatabase: {}",
631 self.workspace_root.display(),
632 self.db_path.display()
633 )
634 };
635
636 Ok(ToolResult::text(status))
637 }
638
639 fn resolve_path(&self, path: &str) -> PathBuf {
642 let p = PathBuf::from(path);
643 if p.is_absolute() {
644 p
645 } else {
646 self.workspace_root.join(p)
647 }
648 }
649
650 fn relative_path(&self, path: &str) -> String {
651 let p = PathBuf::from(path);
652 p.strip_prefix(&self.workspace_root)
653 .map(|p| p.to_string_lossy().to_string())
654 .unwrap_or_else(|_| path.to_string())
655 }
656
657 fn find_symbol_at(
658 &mut self,
659 file: &Path,
660 line: usize,
661 character: usize,
662 ) -> Result<Option<SymbolRecord>> {
663 let store = self.get_store()?;
664 let file_str = file.to_string_lossy().to_string();
665
666 let content = std::fs::read_to_string(file)?;
668 let mut offset: i64 = 0;
669 for (i, l) in content.lines().enumerate() {
670 if i + 1 == line {
671 offset += character.saturating_sub(1) as i64;
672 break;
673 }
674 offset += l.len() as i64 + 1; }
676
677 let symbols = store.list_symbols(Some(&file_str), None, None, None)?;
679
680 let mut best: Option<SymbolRecord> = None;
682 for sym in symbols {
683 if sym.start <= offset && offset < sym.end {
684 let span = sym.end - sym.start;
685 if best
686 .as_ref()
687 .map(|b| span < (b.end - b.start))
688 .unwrap_or(true)
689 {
690 best = Some(sym);
691 }
692 }
693 }
694
695 Ok(best)
696 }
697}
698
699fn format_symbols(symbols: &[SymbolRecord], workspace_root: &Path) -> String {
702 let mut output = String::new();
703 for sym in symbols {
704 output.push_str(&format_symbol(sym, workspace_root));
705 output.push('\n');
706 }
707 output
708}
709
710fn format_symbol(sym: &SymbolRecord, workspace_root: &Path) -> String {
711 let rel_path = PathBuf::from(&sym.file)
712 .strip_prefix(workspace_root)
713 .map(|p| p.to_string_lossy().to_string())
714 .unwrap_or_else(|_| sym.file.clone());
715
716 let location = if let Ok((line, col)) = offset_to_line_col(&sym.file, sym.start as usize) {
718 format!("{}:{}:{}", rel_path, line, col)
719 } else {
720 format!("{}:offset:{}", rel_path, sym.start)
721 };
722
723 let mut parts = vec![format!("{:<10} {:<30} {}", sym.kind, sym.name, location)];
724
725 if let Some(ref vis) = sym.visibility {
726 parts.push(format!(" visibility: {}", vis));
727 }
728 if let Some(ref container) = sym.container {
729 parts.push(format!(" container: {}", container));
730 }
731
732 parts.join("\n")
733}
734
735fn offset_to_line_col(file_path: &str, offset: usize) -> Result<(usize, usize)> {
737 let content = std::fs::read(file_path)?;
738 let mut line = 1;
739 let mut col = 1;
740 for (i, &b) in content.iter().enumerate() {
741 if i == offset {
742 return Ok((line, col));
743 }
744 if b == b'\n' {
745 line += 1;
746 col = 1;
747 } else {
748 col += 1;
749 }
750 }
751 if offset == content.len() {
752 Ok((line, col))
753 } else {
754 anyhow::bail!("offset out of bounds")
755 }
756}
757
758pub fn run_server(workspace_root: &Path, db_path: &Path) -> Result<()> {
760 let mut server = McpServer::new(workspace_root.to_path_buf(), db_path.to_path_buf());
761 server.run()
762}