1use std::collections::HashMap;
8
9use serde_json::{json, Value};
10
11use crate::engine::query::{ImpactParams, MatchMode, SymbolLookupParams};
12use crate::engine::QueryEngine;
13use crate::graph::CodeGraph;
14use crate::types::{CodeUnitType, EdgeType};
15
16use super::protocol::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
17
18const SERVER_NAME: &str = "agentic-codebase";
20const SERVER_VERSION: &str = "0.1.0";
22const PROTOCOL_VERSION: &str = "2024-11-05";
24
25#[derive(Debug, Clone)]
27pub struct OperationRecord {
28 pub tool_name: String,
29 pub summary: String,
30 pub timestamp: u64,
31 pub graph_name: Option<String>,
32}
33
34#[derive(Debug)]
39pub struct McpServer {
40 graphs: HashMap<String, CodeGraph>,
42 engine: QueryEngine,
44 initialized: bool,
46 operation_log: Vec<OperationRecord>,
48 session_start_time: Option<u64>,
50}
51
52impl McpServer {
53 fn parse_unit_type(raw: &str) -> Option<CodeUnitType> {
54 match raw.trim().to_ascii_lowercase().as_str() {
55 "module" | "modules" => Some(CodeUnitType::Module),
56 "symbol" | "symbols" => Some(CodeUnitType::Symbol),
57 "type" | "types" => Some(CodeUnitType::Type),
58 "function" | "functions" => Some(CodeUnitType::Function),
59 "parameter" | "parameters" => Some(CodeUnitType::Parameter),
60 "import" | "imports" => Some(CodeUnitType::Import),
61 "test" | "tests" => Some(CodeUnitType::Test),
62 "doc" | "docs" | "document" | "documents" => Some(CodeUnitType::Doc),
63 "config" | "configs" => Some(CodeUnitType::Config),
64 "pattern" | "patterns" => Some(CodeUnitType::Pattern),
65 "trait" | "traits" => Some(CodeUnitType::Trait),
66 "impl" | "implementation" | "implementations" => Some(CodeUnitType::Impl),
67 "macro" | "macros" => Some(CodeUnitType::Macro),
68 _ => None,
69 }
70 }
71
72 pub fn new() -> Self {
74 Self {
75 graphs: HashMap::new(),
76 engine: QueryEngine::new(),
77 initialized: false,
78 operation_log: Vec::new(),
79 session_start_time: None,
80 }
81 }
82
83 pub fn load_graph(&mut self, name: String, graph: CodeGraph) {
85 self.graphs.insert(name, graph);
86 }
87
88 pub fn unload_graph(&mut self, name: &str) -> Option<CodeGraph> {
90 self.graphs.remove(name)
91 }
92
93 pub fn get_graph(&self, name: &str) -> Option<&CodeGraph> {
95 self.graphs.get(name)
96 }
97
98 pub fn graph_names(&self) -> Vec<&str> {
100 self.graphs.keys().map(|s| s.as_str()).collect()
101 }
102
103 pub fn is_initialized(&self) -> bool {
105 self.initialized
106 }
107
108 pub fn handle_raw(&mut self, raw: &str) -> String {
113 let response = match super::protocol::parse_request(raw) {
114 Ok(request) => {
115 if request.id.is_none() {
116 self.handle_notification(&request.method, &request.params);
117 return String::new();
118 }
119 self.handle_request(request)
120 }
121 Err(error_response) => error_response,
122 };
123 serde_json::to_string(&response).unwrap_or_else(|_| {
124 r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Serialization failed"}}"#
125 .to_string()
126 })
127 }
128
129 pub fn handle_request(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
131 let id = request.id.clone().unwrap_or(Value::Null);
132 match request.method.as_str() {
133 "initialize" => self.handle_initialize(id, &request.params),
134 "shutdown" => self.handle_shutdown(id),
135 "tools/list" => self.handle_tools_list(id),
136 "tools/call" => self.handle_tools_call(id, &request.params),
137 "resources/list" => self.handle_resources_list(id),
138 "resources/read" => self.handle_resources_read(id, &request.params),
139 "prompts/list" => self.handle_prompts_list(id),
140 _ => JsonRpcResponse::error(id, JsonRpcError::method_not_found(&request.method)),
141 }
142 }
143
144 fn handle_notification(&mut self, method: &str, _params: &Value) {
148 if method == "notifications/initialized" {
149 self.initialized = true;
150 }
151 }
152
153 fn handle_initialize(&mut self, id: Value, _params: &Value) -> JsonRpcResponse {
159 self.initialized = true;
160 self.session_start_time = Some(
161 std::time::SystemTime::now()
162 .duration_since(std::time::UNIX_EPOCH)
163 .unwrap_or_default()
164 .as_secs(),
165 );
166 self.operation_log.clear();
167 JsonRpcResponse::success(
168 id,
169 json!({
170 "protocolVersion": PROTOCOL_VERSION,
171 "capabilities": {
172 "tools": { "listChanged": false },
173 "resources": { "subscribe": false, "listChanged": false },
174 "prompts": { "listChanged": false }
175 },
176 "serverInfo": {
177 "name": SERVER_NAME,
178 "version": SERVER_VERSION
179 }
180 }),
181 )
182 }
183
184 fn handle_shutdown(&mut self, id: Value) -> JsonRpcResponse {
186 self.initialized = false;
187 JsonRpcResponse::success(id, json!(null))
188 }
189
190 fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
192 JsonRpcResponse::success(
193 id,
194 json!({
195 "tools": [
196 {
197 "name": "symbol_lookup",
198 "description": "Look up symbols by name in the code graph.",
199 "inputSchema": {
200 "type": "object",
201 "properties": {
202 "graph": { "type": "string", "description": "Graph name" },
203 "name": { "type": "string", "description": "Symbol name to search for" },
204 "mode": { "type": "string", "enum": ["exact", "prefix", "contains", "fuzzy"], "default": "prefix" },
205 "limit": { "type": "integer", "minimum": 1, "default": 10 }
206 },
207 "required": ["name"]
208 }
209 },
210 {
211 "name": "impact_analysis",
212 "description": "Analyse the impact of changing a code unit.",
213 "inputSchema": {
214 "type": "object",
215 "properties": {
216 "graph": { "type": "string", "description": "Graph name" },
217 "unit_id": { "type": "integer", "description": "Code unit ID to analyse" },
218 "max_depth": { "type": "integer", "minimum": 0, "default": 3 }
219 },
220 "required": ["unit_id"]
221 }
222 },
223 {
224 "name": "graph_stats",
225 "description": "Get summary statistics about a loaded code graph.",
226 "inputSchema": {
227 "type": "object",
228 "properties": {
229 "graph": { "type": "string", "description": "Graph name" }
230 }
231 }
232 },
233 {
234 "name": "list_units",
235 "description": "List code units in a graph, optionally filtered by type.",
236 "inputSchema": {
237 "type": "object",
238 "properties": {
239 "graph": { "type": "string", "description": "Graph name" },
240 "unit_type": {
241 "type": "string",
242 "description": "Filter by unit type",
243 "enum": [
244 "module", "symbol", "type", "function", "parameter", "import",
245 "test", "doc", "config", "pattern", "trait", "impl", "macro"
246 ]
247 },
248 "limit": { "type": "integer", "default": 50 }
249 }
250 }
251 },
252 {
253 "name": "analysis_log",
254 "description": "Log the intent and context behind a code analysis. Call this to record WHY you are performing a lookup or analysis.",
255 "inputSchema": {
256 "type": "object",
257 "properties": {
258 "intent": {
259 "type": "string",
260 "description": "Why you are analysing — the goal or reason for the code query"
261 },
262 "finding": {
263 "type": "string",
264 "description": "What you found or concluded from the analysis"
265 },
266 "graph": {
267 "type": "string",
268 "description": "Optional graph name this analysis relates to"
269 },
270 "topic": {
271 "type": "string",
272 "description": "Optional topic or category (e.g., 'refactoring', 'bug-hunt')"
273 }
274 },
275 "required": ["intent"]
276 }
277 }
278 ]
279 }),
280 )
281 }
282
283 fn handle_tools_call(&mut self, id: Value, params: &Value) -> JsonRpcResponse {
285 let tool_name = match params.get("name").and_then(|v| v.as_str()) {
286 Some(name) => name,
287 None => {
288 return JsonRpcResponse::error(
289 id,
290 JsonRpcError::invalid_params("Missing 'name' field in tools/call params"),
291 );
292 }
293 };
294
295 let arguments = params
296 .get("arguments")
297 .cloned()
298 .unwrap_or(Value::Object(serde_json::Map::new()));
299
300 let result = match tool_name {
301 "symbol_lookup" => self.tool_symbol_lookup(id.clone(), &arguments),
302 "impact_analysis" => self.tool_impact_analysis(id.clone(), &arguments),
303 "graph_stats" => self.tool_graph_stats(id.clone(), &arguments),
304 "list_units" => self.tool_list_units(id.clone(), &arguments),
305 "analysis_log" => return self.tool_analysis_log(id, &arguments),
306 _ => {
307 return JsonRpcResponse::error(
308 id,
309 JsonRpcError::method_not_found(format!("Unknown tool: {}", tool_name)),
310 );
311 }
312 };
313
314 let now = std::time::SystemTime::now()
316 .duration_since(std::time::UNIX_EPOCH)
317 .unwrap_or_default()
318 .as_secs();
319 let summary = truncate_json_summary(&arguments, 200);
320 let graph_name = arguments
321 .get("graph")
322 .and_then(|v| v.as_str())
323 .map(String::from);
324 self.operation_log.push(OperationRecord {
325 tool_name: tool_name.to_string(),
326 summary,
327 timestamp: now,
328 graph_name,
329 });
330
331 result
332 }
333
334 fn handle_resources_list(&self, id: Value) -> JsonRpcResponse {
336 let mut resources = Vec::new();
337
338 for name in self.graphs.keys() {
339 resources.push(json!({
340 "uri": format!("acb://graphs/{}/stats", name),
341 "name": format!("{} statistics", name),
342 "description": format!("Statistics for the {} code graph.", name),
343 "mimeType": "application/json"
344 }));
345 resources.push(json!({
346 "uri": format!("acb://graphs/{}/units", name),
347 "name": format!("{} units", name),
348 "description": format!("All code units in the {} graph.", name),
349 "mimeType": "application/json"
350 }));
351 }
352
353 JsonRpcResponse::success(id, json!({ "resources": resources }))
354 }
355
356 fn handle_resources_read(&self, id: Value, params: &Value) -> JsonRpcResponse {
358 let uri = match params.get("uri").and_then(|v| v.as_str()) {
359 Some(u) => u,
360 None => {
361 return JsonRpcResponse::error(
362 id,
363 JsonRpcError::invalid_params("Missing 'uri' field"),
364 );
365 }
366 };
367
368 if let Some(rest) = uri.strip_prefix("acb://graphs/") {
370 let parts: Vec<&str> = rest.splitn(2, '/').collect();
371 if parts.len() == 2 {
372 let graph_name = parts[0];
373 let resource = parts[1];
374
375 if let Some(graph) = self.graphs.get(graph_name) {
376 return match resource {
377 "stats" => {
378 let stats = graph.stats();
379 JsonRpcResponse::success(
380 id,
381 json!({
382 "contents": [{
383 "uri": uri,
384 "mimeType": "application/json",
385 "text": serde_json::to_string_pretty(&json!({
386 "unit_count": stats.unit_count,
387 "edge_count": stats.edge_count,
388 "dimension": stats.dimension,
389 })).unwrap_or_default()
390 }]
391 }),
392 )
393 }
394 "units" => {
395 let units: Vec<Value> = graph
396 .units()
397 .iter()
398 .map(|u| {
399 json!({
400 "id": u.id,
401 "name": u.name,
402 "type": u.unit_type.label(),
403 "file": u.file_path.display().to_string(),
404 })
405 })
406 .collect();
407 JsonRpcResponse::success(
408 id,
409 json!({
410 "contents": [{
411 "uri": uri,
412 "mimeType": "application/json",
413 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
414 }]
415 }),
416 )
417 }
418 _ => JsonRpcResponse::error(
419 id,
420 JsonRpcError::invalid_params(format!(
421 "Unknown resource type: {}",
422 resource
423 )),
424 ),
425 };
426 } else {
427 return JsonRpcResponse::error(
428 id,
429 JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)),
430 );
431 }
432 }
433 }
434
435 JsonRpcResponse::error(
436 id,
437 JsonRpcError::invalid_params(format!("Invalid resource URI: {}", uri)),
438 )
439 }
440
441 fn handle_prompts_list(&self, id: Value) -> JsonRpcResponse {
443 JsonRpcResponse::success(
444 id,
445 json!({
446 "prompts": [
447 {
448 "name": "analyse_unit",
449 "description": "Analyse a code unit including its dependencies, stability, and test coverage.",
450 "arguments": [
451 {
452 "name": "graph",
453 "description": "Graph name",
454 "required": false
455 },
456 {
457 "name": "unit_name",
458 "description": "Name of the code unit to analyse",
459 "required": true
460 }
461 ]
462 },
463 {
464 "name": "explain_coupling",
465 "description": "Explain coupling between two code units.",
466 "arguments": [
467 {
468 "name": "graph",
469 "description": "Graph name",
470 "required": false
471 },
472 {
473 "name": "unit_a",
474 "description": "First unit name",
475 "required": true
476 },
477 {
478 "name": "unit_b",
479 "description": "Second unit name",
480 "required": true
481 }
482 ]
483 }
484 ]
485 }),
486 )
487 }
488
489 fn resolve_graph<'a>(
495 &'a self,
496 args: &'a Value,
497 ) -> Result<(&'a str, &'a CodeGraph), JsonRpcError> {
498 let graph_name = args.get("graph").and_then(|v| v.as_str()).unwrap_or("");
499
500 if graph_name.is_empty() {
501 if let Some((name, graph)) = self.graphs.iter().next() {
503 return Ok((name.as_str(), graph));
504 }
505 return Err(JsonRpcError::invalid_params("No graphs loaded"));
506 }
507
508 self.graphs
509 .get(graph_name)
510 .map(|g| (graph_name, g))
511 .ok_or_else(|| JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)))
512 }
513
514 fn tool_symbol_lookup(&self, id: Value, args: &Value) -> JsonRpcResponse {
516 let (_, graph) = match self.resolve_graph(args) {
517 Ok(g) => g,
518 Err(e) => return JsonRpcResponse::error(id, e),
519 };
520
521 let name = match args.get("name").and_then(|v| v.as_str()) {
522 Some(n) => n.to_string(),
523 None => {
524 return JsonRpcResponse::error(
525 id,
526 JsonRpcError::invalid_params("Missing 'name' argument"),
527 );
528 }
529 };
530
531 let mode_raw = args
532 .get("mode")
533 .and_then(|v| v.as_str())
534 .unwrap_or("prefix");
535 let mode = match mode_raw {
536 "exact" => MatchMode::Exact,
537 "prefix" => MatchMode::Prefix,
538 "contains" => MatchMode::Contains,
539 "fuzzy" => MatchMode::Fuzzy,
540 _ => {
541 return JsonRpcResponse::error(
542 id,
543 JsonRpcError::invalid_params(format!(
544 "Invalid 'mode': {mode_raw}. Expected one of: exact, prefix, contains, fuzzy"
545 )),
546 );
547 }
548 };
549
550 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
551
552 let params = SymbolLookupParams {
553 name,
554 mode,
555 limit,
556 ..SymbolLookupParams::default()
557 };
558
559 match self.engine.symbol_lookup(graph, params) {
560 Ok(units) => {
561 let results: Vec<Value> = units
562 .iter()
563 .map(|u| {
564 json!({
565 "id": u.id,
566 "name": u.name,
567 "qualified_name": u.qualified_name,
568 "type": u.unit_type.label(),
569 "file": u.file_path.display().to_string(),
570 "language": u.language.name(),
571 "complexity": u.complexity,
572 })
573 })
574 .collect();
575 JsonRpcResponse::success(
576 id,
577 json!({
578 "content": [{
579 "type": "text",
580 "text": serde_json::to_string_pretty(&results).unwrap_or_default()
581 }]
582 }),
583 )
584 }
585 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
586 }
587 }
588
589 fn tool_impact_analysis(&self, id: Value, args: &Value) -> JsonRpcResponse {
591 let (_, graph) = match self.resolve_graph(args) {
592 Ok(g) => g,
593 Err(e) => return JsonRpcResponse::error(id, e),
594 };
595
596 let unit_id = match args.get("unit_id").and_then(|v| v.as_u64()) {
597 Some(uid) => uid,
598 None => {
599 return JsonRpcResponse::error(
600 id,
601 JsonRpcError::invalid_params("Missing 'unit_id' argument"),
602 );
603 }
604 };
605
606 let max_depth = match args.get("max_depth") {
607 None => 3,
608 Some(v) => {
609 let depth = match v.as_i64() {
610 Some(d) => d,
611 None => {
612 return JsonRpcResponse::error(
613 id,
614 JsonRpcError::invalid_params("'max_depth' must be an integer >= 0"),
615 );
616 }
617 };
618 if depth < 0 {
619 return JsonRpcResponse::error(
620 id,
621 JsonRpcError::invalid_params("'max_depth' must be >= 0"),
622 );
623 }
624 depth as u32
625 }
626 };
627 let edge_types = vec![
628 EdgeType::Calls,
629 EdgeType::Imports,
630 EdgeType::Inherits,
631 EdgeType::Implements,
632 EdgeType::UsesType,
633 EdgeType::FfiBinds,
634 EdgeType::References,
635 EdgeType::Returns,
636 EdgeType::ParamType,
637 EdgeType::Overrides,
638 EdgeType::Contains,
639 ];
640
641 let params = ImpactParams {
642 unit_id,
643 max_depth,
644 edge_types,
645 };
646
647 match self.engine.impact_analysis(graph, params) {
648 Ok(result) => {
649 let impacted: Vec<Value> = result
650 .impacted
651 .iter()
652 .map(|i| {
653 json!({
654 "unit_id": i.unit_id,
655 "depth": i.depth,
656 "risk_score": i.risk_score,
657 "has_tests": i.has_tests,
658 })
659 })
660 .collect();
661 JsonRpcResponse::success(
662 id,
663 json!({
664 "content": [{
665 "type": "text",
666 "text": serde_json::to_string_pretty(&json!({
667 "root_id": result.root_id,
668 "overall_risk": result.overall_risk,
669 "impacted_count": result.impacted.len(),
670 "impacted": impacted,
671 "recommendations": result.recommendations,
672 })).unwrap_or_default()
673 }]
674 }),
675 )
676 }
677 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
678 }
679 }
680
681 fn tool_graph_stats(&self, id: Value, args: &Value) -> JsonRpcResponse {
683 let (name, graph) = match self.resolve_graph(args) {
684 Ok(g) => g,
685 Err(e) => return JsonRpcResponse::error(id, e),
686 };
687
688 let stats = graph.stats();
689 JsonRpcResponse::success(
690 id,
691 json!({
692 "content": [{
693 "type": "text",
694 "text": serde_json::to_string_pretty(&json!({
695 "graph": name,
696 "unit_count": stats.unit_count,
697 "edge_count": stats.edge_count,
698 "dimension": stats.dimension,
699 })).unwrap_or_default()
700 }]
701 }),
702 )
703 }
704
705 fn tool_list_units(&self, id: Value, args: &Value) -> JsonRpcResponse {
707 let (_, graph) = match self.resolve_graph(args) {
708 Ok(g) => g,
709 Err(e) => return JsonRpcResponse::error(id, e),
710 };
711
712 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
713 let unit_type_filter = match args.get("unit_type").and_then(|v| v.as_str()) {
714 Some(raw) => match Self::parse_unit_type(raw) {
715 Some(parsed) => Some(parsed),
716 None => {
717 return JsonRpcResponse::error(
718 id,
719 JsonRpcError::invalid_params(format!(
720 "Unknown unit_type '{}'. Expected one of: module, symbol, type, function, parameter, import, test, doc, config, pattern, trait, impl, macro.",
721 raw
722 )),
723 );
724 }
725 },
726 None => None,
727 };
728
729 let units: Vec<Value> = graph
730 .units()
731 .iter()
732 .filter(|u| {
733 if let Some(expected) = unit_type_filter {
734 u.unit_type == expected
735 } else {
736 true
737 }
738 })
739 .take(limit)
740 .map(|u| {
741 json!({
742 "id": u.id,
743 "name": u.name,
744 "type": u.unit_type.label(),
745 "file": u.file_path.display().to_string(),
746 })
747 })
748 .collect();
749
750 JsonRpcResponse::success(
751 id,
752 json!({
753 "content": [{
754 "type": "text",
755 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
756 }]
757 }),
758 )
759 }
760
761 fn tool_analysis_log(&mut self, id: Value, args: &Value) -> JsonRpcResponse {
763 let intent = match args.get("intent").and_then(|v| v.as_str()) {
764 Some(i) if !i.trim().is_empty() => i,
765 _ => {
766 return JsonRpcResponse::error(
767 id,
768 JsonRpcError::invalid_params("'intent' is required and must not be empty"),
769 );
770 }
771 };
772
773 let finding = args.get("finding").and_then(|v| v.as_str());
774 let graph_name = args.get("graph").and_then(|v| v.as_str());
775 let topic = args.get("topic").and_then(|v| v.as_str());
776
777 let now = std::time::SystemTime::now()
778 .duration_since(std::time::UNIX_EPOCH)
779 .unwrap_or_default()
780 .as_secs();
781
782 let mut summary_parts = vec![format!("intent: {intent}")];
783 if let Some(f) = finding {
784 summary_parts.push(format!("finding: {f}"));
785 }
786 if let Some(t) = topic {
787 summary_parts.push(format!("topic: {t}"));
788 }
789
790 let record = OperationRecord {
791 tool_name: "analysis_log".to_string(),
792 summary: summary_parts.join(" | "),
793 timestamp: now,
794 graph_name: graph_name.map(String::from),
795 };
796
797 let index = self.operation_log.len();
798 self.operation_log.push(record);
799
800 JsonRpcResponse::success(
801 id,
802 json!({
803 "content": [{
804 "type": "text",
805 "text": serde_json::to_string_pretty(&json!({
806 "log_index": index,
807 "message": "Analysis context logged"
808 })).unwrap_or_default()
809 }]
810 }),
811 )
812 }
813
814 pub fn operation_log(&self) -> &[OperationRecord] {
816 &self.operation_log
817 }
818}
819
820fn truncate_json_summary(value: &Value, max_len: usize) -> String {
822 let s = value.to_string();
823 if s.len() <= max_len {
824 s
825 } else {
826 format!("{}...", &s[..max_len])
827 }
828}
829
830impl Default for McpServer {
831 fn default() -> Self {
832 Self::new()
833 }
834}