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)]
30pub struct McpServer {
31 graphs: HashMap<String, CodeGraph>,
33 engine: QueryEngine,
35 initialized: bool,
37}
38
39impl McpServer {
40 fn parse_unit_type(raw: &str) -> Option<CodeUnitType> {
41 match raw.trim().to_ascii_lowercase().as_str() {
42 "module" | "modules" => Some(CodeUnitType::Module),
43 "symbol" | "symbols" => Some(CodeUnitType::Symbol),
44 "type" | "types" => Some(CodeUnitType::Type),
45 "function" | "functions" => Some(CodeUnitType::Function),
46 "parameter" | "parameters" => Some(CodeUnitType::Parameter),
47 "import" | "imports" => Some(CodeUnitType::Import),
48 "test" | "tests" => Some(CodeUnitType::Test),
49 "doc" | "docs" | "document" | "documents" => Some(CodeUnitType::Doc),
50 "config" | "configs" => Some(CodeUnitType::Config),
51 "pattern" | "patterns" => Some(CodeUnitType::Pattern),
52 "trait" | "traits" => Some(CodeUnitType::Trait),
53 "impl" | "implementation" | "implementations" => Some(CodeUnitType::Impl),
54 "macro" | "macros" => Some(CodeUnitType::Macro),
55 _ => None,
56 }
57 }
58
59 pub fn new() -> Self {
61 Self {
62 graphs: HashMap::new(),
63 engine: QueryEngine::new(),
64 initialized: false,
65 }
66 }
67
68 pub fn load_graph(&mut self, name: String, graph: CodeGraph) {
70 self.graphs.insert(name, graph);
71 }
72
73 pub fn unload_graph(&mut self, name: &str) -> Option<CodeGraph> {
75 self.graphs.remove(name)
76 }
77
78 pub fn get_graph(&self, name: &str) -> Option<&CodeGraph> {
80 self.graphs.get(name)
81 }
82
83 pub fn graph_names(&self) -> Vec<&str> {
85 self.graphs.keys().map(|s| s.as_str()).collect()
86 }
87
88 pub fn is_initialized(&self) -> bool {
90 self.initialized
91 }
92
93 pub fn handle_raw(&mut self, raw: &str) -> String {
98 let response = match super::protocol::parse_request(raw) {
99 Ok(request) => {
100 if request.id.is_none() {
101 self.handle_notification(&request.method, &request.params);
102 return String::new();
103 }
104 self.handle_request(request)
105 }
106 Err(error_response) => error_response,
107 };
108 serde_json::to_string(&response).unwrap_or_else(|_| {
109 r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Serialization failed"}}"#
110 .to_string()
111 })
112 }
113
114 pub fn handle_request(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
116 let id = request.id.clone().unwrap_or(Value::Null);
117 match request.method.as_str() {
118 "initialize" => self.handle_initialize(id, &request.params),
119 "shutdown" => self.handle_shutdown(id),
120 "tools/list" => self.handle_tools_list(id),
121 "tools/call" => self.handle_tools_call(id, &request.params),
122 "resources/list" => self.handle_resources_list(id),
123 "resources/read" => self.handle_resources_read(id, &request.params),
124 "prompts/list" => self.handle_prompts_list(id),
125 _ => JsonRpcResponse::error(id, JsonRpcError::method_not_found(&request.method)),
126 }
127 }
128
129 fn handle_notification(&mut self, method: &str, _params: &Value) {
133 if method == "notifications/initialized" {
134 self.initialized = true;
135 }
136 }
137
138 fn handle_initialize(&mut self, id: Value, _params: &Value) -> JsonRpcResponse {
144 self.initialized = true;
145 JsonRpcResponse::success(
146 id,
147 json!({
148 "protocolVersion": PROTOCOL_VERSION,
149 "capabilities": {
150 "tools": { "listChanged": false },
151 "resources": { "subscribe": false, "listChanged": false },
152 "prompts": { "listChanged": false }
153 },
154 "serverInfo": {
155 "name": SERVER_NAME,
156 "version": SERVER_VERSION
157 }
158 }),
159 )
160 }
161
162 fn handle_shutdown(&mut self, id: Value) -> JsonRpcResponse {
164 self.initialized = false;
165 JsonRpcResponse::success(id, json!(null))
166 }
167
168 fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
170 JsonRpcResponse::success(
171 id,
172 json!({
173 "tools": [
174 {
175 "name": "symbol_lookup",
176 "description": "Look up symbols by name in the code graph.",
177 "inputSchema": {
178 "type": "object",
179 "properties": {
180 "graph": { "type": "string", "description": "Graph name" },
181 "name": { "type": "string", "description": "Symbol name to search for" },
182 "mode": { "type": "string", "enum": ["exact", "prefix", "contains", "fuzzy"], "default": "prefix" },
183 "limit": { "type": "integer", "minimum": 1, "default": 10 }
184 },
185 "required": ["name"]
186 }
187 },
188 {
189 "name": "impact_analysis",
190 "description": "Analyse the impact of changing a code unit.",
191 "inputSchema": {
192 "type": "object",
193 "properties": {
194 "graph": { "type": "string", "description": "Graph name" },
195 "unit_id": { "type": "integer", "description": "Code unit ID to analyse" },
196 "max_depth": { "type": "integer", "minimum": 0, "default": 3 }
197 },
198 "required": ["unit_id"]
199 }
200 },
201 {
202 "name": "graph_stats",
203 "description": "Get summary statistics about a loaded code graph.",
204 "inputSchema": {
205 "type": "object",
206 "properties": {
207 "graph": { "type": "string", "description": "Graph name" }
208 }
209 }
210 },
211 {
212 "name": "list_units",
213 "description": "List code units in a graph, optionally filtered by type.",
214 "inputSchema": {
215 "type": "object",
216 "properties": {
217 "graph": { "type": "string", "description": "Graph name" },
218 "unit_type": {
219 "type": "string",
220 "description": "Filter by unit type",
221 "enum": [
222 "module", "symbol", "type", "function", "parameter", "import",
223 "test", "doc", "config", "pattern", "trait", "impl", "macro"
224 ]
225 },
226 "limit": { "type": "integer", "default": 50 }
227 }
228 }
229 }
230 ]
231 }),
232 )
233 }
234
235 fn handle_tools_call(&self, id: Value, params: &Value) -> JsonRpcResponse {
237 let tool_name = match params.get("name").and_then(|v| v.as_str()) {
238 Some(name) => name,
239 None => {
240 return JsonRpcResponse::error(
241 id,
242 JsonRpcError::invalid_params("Missing 'name' field in tools/call params"),
243 );
244 }
245 };
246
247 let arguments = params
248 .get("arguments")
249 .cloned()
250 .unwrap_or(Value::Object(serde_json::Map::new()));
251
252 match tool_name {
253 "symbol_lookup" => self.tool_symbol_lookup(id, &arguments),
254 "impact_analysis" => self.tool_impact_analysis(id, &arguments),
255 "graph_stats" => self.tool_graph_stats(id, &arguments),
256 "list_units" => self.tool_list_units(id, &arguments),
257 _ => JsonRpcResponse::error(
258 id,
259 JsonRpcError::method_not_found(format!("Unknown tool: {}", tool_name)),
260 ),
261 }
262 }
263
264 fn handle_resources_list(&self, id: Value) -> JsonRpcResponse {
266 let mut resources = Vec::new();
267
268 for name in self.graphs.keys() {
269 resources.push(json!({
270 "uri": format!("acb://graphs/{}/stats", name),
271 "name": format!("{} statistics", name),
272 "description": format!("Statistics for the {} code graph.", name),
273 "mimeType": "application/json"
274 }));
275 resources.push(json!({
276 "uri": format!("acb://graphs/{}/units", name),
277 "name": format!("{} units", name),
278 "description": format!("All code units in the {} graph.", name),
279 "mimeType": "application/json"
280 }));
281 }
282
283 JsonRpcResponse::success(id, json!({ "resources": resources }))
284 }
285
286 fn handle_resources_read(&self, id: Value, params: &Value) -> JsonRpcResponse {
288 let uri = match params.get("uri").and_then(|v| v.as_str()) {
289 Some(u) => u,
290 None => {
291 return JsonRpcResponse::error(
292 id,
293 JsonRpcError::invalid_params("Missing 'uri' field"),
294 );
295 }
296 };
297
298 if let Some(rest) = uri.strip_prefix("acb://graphs/") {
300 let parts: Vec<&str> = rest.splitn(2, '/').collect();
301 if parts.len() == 2 {
302 let graph_name = parts[0];
303 let resource = parts[1];
304
305 if let Some(graph) = self.graphs.get(graph_name) {
306 return match resource {
307 "stats" => {
308 let stats = graph.stats();
309 JsonRpcResponse::success(
310 id,
311 json!({
312 "contents": [{
313 "uri": uri,
314 "mimeType": "application/json",
315 "text": serde_json::to_string_pretty(&json!({
316 "unit_count": stats.unit_count,
317 "edge_count": stats.edge_count,
318 "dimension": stats.dimension,
319 })).unwrap_or_default()
320 }]
321 }),
322 )
323 }
324 "units" => {
325 let units: Vec<Value> = graph
326 .units()
327 .iter()
328 .map(|u| {
329 json!({
330 "id": u.id,
331 "name": u.name,
332 "type": u.unit_type.label(),
333 "file": u.file_path.display().to_string(),
334 })
335 })
336 .collect();
337 JsonRpcResponse::success(
338 id,
339 json!({
340 "contents": [{
341 "uri": uri,
342 "mimeType": "application/json",
343 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
344 }]
345 }),
346 )
347 }
348 _ => JsonRpcResponse::error(
349 id,
350 JsonRpcError::invalid_params(format!(
351 "Unknown resource type: {}",
352 resource
353 )),
354 ),
355 };
356 } else {
357 return JsonRpcResponse::error(
358 id,
359 JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)),
360 );
361 }
362 }
363 }
364
365 JsonRpcResponse::error(
366 id,
367 JsonRpcError::invalid_params(format!("Invalid resource URI: {}", uri)),
368 )
369 }
370
371 fn handle_prompts_list(&self, id: Value) -> JsonRpcResponse {
373 JsonRpcResponse::success(
374 id,
375 json!({
376 "prompts": [
377 {
378 "name": "analyse_unit",
379 "description": "Analyse a code unit including its dependencies, stability, and test coverage.",
380 "arguments": [
381 {
382 "name": "graph",
383 "description": "Graph name",
384 "required": false
385 },
386 {
387 "name": "unit_name",
388 "description": "Name of the code unit to analyse",
389 "required": true
390 }
391 ]
392 },
393 {
394 "name": "explain_coupling",
395 "description": "Explain coupling between two code units.",
396 "arguments": [
397 {
398 "name": "graph",
399 "description": "Graph name",
400 "required": false
401 },
402 {
403 "name": "unit_a",
404 "description": "First unit name",
405 "required": true
406 },
407 {
408 "name": "unit_b",
409 "description": "Second unit name",
410 "required": true
411 }
412 ]
413 }
414 ]
415 }),
416 )
417 }
418
419 fn resolve_graph<'a>(
425 &'a self,
426 args: &'a Value,
427 ) -> Result<(&'a str, &'a CodeGraph), JsonRpcError> {
428 let graph_name = args.get("graph").and_then(|v| v.as_str()).unwrap_or("");
429
430 if graph_name.is_empty() {
431 if let Some((name, graph)) = self.graphs.iter().next() {
433 return Ok((name.as_str(), graph));
434 }
435 return Err(JsonRpcError::invalid_params("No graphs loaded"));
436 }
437
438 self.graphs
439 .get(graph_name)
440 .map(|g| (graph_name, g))
441 .ok_or_else(|| JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)))
442 }
443
444 fn tool_symbol_lookup(&self, id: Value, args: &Value) -> JsonRpcResponse {
446 let (_, graph) = match self.resolve_graph(args) {
447 Ok(g) => g,
448 Err(e) => return JsonRpcResponse::error(id, e),
449 };
450
451 let name = match args.get("name").and_then(|v| v.as_str()) {
452 Some(n) => n.to_string(),
453 None => {
454 return JsonRpcResponse::error(
455 id,
456 JsonRpcError::invalid_params("Missing 'name' argument"),
457 );
458 }
459 };
460
461 let mode_raw = args
462 .get("mode")
463 .and_then(|v| v.as_str())
464 .unwrap_or("prefix");
465 let mode = match mode_raw {
466 "exact" => MatchMode::Exact,
467 "prefix" => MatchMode::Prefix,
468 "contains" => MatchMode::Contains,
469 "fuzzy" => MatchMode::Fuzzy,
470 _ => {
471 return JsonRpcResponse::error(
472 id,
473 JsonRpcError::invalid_params(format!(
474 "Invalid 'mode': {mode_raw}. Expected one of: exact, prefix, contains, fuzzy"
475 )),
476 );
477 }
478 };
479
480 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
481
482 let params = SymbolLookupParams {
483 name,
484 mode,
485 limit,
486 ..SymbolLookupParams::default()
487 };
488
489 match self.engine.symbol_lookup(graph, params) {
490 Ok(units) => {
491 let results: Vec<Value> = units
492 .iter()
493 .map(|u| {
494 json!({
495 "id": u.id,
496 "name": u.name,
497 "qualified_name": u.qualified_name,
498 "type": u.unit_type.label(),
499 "file": u.file_path.display().to_string(),
500 "language": u.language.name(),
501 "complexity": u.complexity,
502 })
503 })
504 .collect();
505 JsonRpcResponse::success(
506 id,
507 json!({
508 "content": [{
509 "type": "text",
510 "text": serde_json::to_string_pretty(&results).unwrap_or_default()
511 }]
512 }),
513 )
514 }
515 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
516 }
517 }
518
519 fn tool_impact_analysis(&self, id: Value, args: &Value) -> JsonRpcResponse {
521 let (_, graph) = match self.resolve_graph(args) {
522 Ok(g) => g,
523 Err(e) => return JsonRpcResponse::error(id, e),
524 };
525
526 let unit_id = match args.get("unit_id").and_then(|v| v.as_u64()) {
527 Some(uid) => uid,
528 None => {
529 return JsonRpcResponse::error(
530 id,
531 JsonRpcError::invalid_params("Missing 'unit_id' argument"),
532 );
533 }
534 };
535
536 let max_depth = match args.get("max_depth") {
537 None => 3,
538 Some(v) => {
539 let depth = match v.as_i64() {
540 Some(d) => d,
541 None => {
542 return JsonRpcResponse::error(
543 id,
544 JsonRpcError::invalid_params(
545 "'max_depth' must be an integer >= 0",
546 ),
547 );
548 }
549 };
550 if depth < 0 {
551 return JsonRpcResponse::error(
552 id,
553 JsonRpcError::invalid_params(
554 "'max_depth' must be >= 0",
555 ),
556 );
557 }
558 depth as u32
559 }
560 };
561 let edge_types = vec![
562 EdgeType::Calls,
563 EdgeType::Imports,
564 EdgeType::Inherits,
565 EdgeType::Implements,
566 EdgeType::UsesType,
567 EdgeType::FfiBinds,
568 EdgeType::References,
569 EdgeType::Returns,
570 EdgeType::ParamType,
571 EdgeType::Overrides,
572 EdgeType::Contains,
573 ];
574
575 let params = ImpactParams {
576 unit_id,
577 max_depth,
578 edge_types,
579 };
580
581 match self.engine.impact_analysis(graph, params) {
582 Ok(result) => {
583 let impacted: Vec<Value> = result
584 .impacted
585 .iter()
586 .map(|i| {
587 json!({
588 "unit_id": i.unit_id,
589 "depth": i.depth,
590 "risk_score": i.risk_score,
591 "has_tests": i.has_tests,
592 })
593 })
594 .collect();
595 JsonRpcResponse::success(
596 id,
597 json!({
598 "content": [{
599 "type": "text",
600 "text": serde_json::to_string_pretty(&json!({
601 "root_id": result.root_id,
602 "overall_risk": result.overall_risk,
603 "impacted_count": result.impacted.len(),
604 "impacted": impacted,
605 "recommendations": result.recommendations,
606 })).unwrap_or_default()
607 }]
608 }),
609 )
610 }
611 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
612 }
613 }
614
615 fn tool_graph_stats(&self, id: Value, args: &Value) -> JsonRpcResponse {
617 let (name, graph) = match self.resolve_graph(args) {
618 Ok(g) => g,
619 Err(e) => return JsonRpcResponse::error(id, e),
620 };
621
622 let stats = graph.stats();
623 JsonRpcResponse::success(
624 id,
625 json!({
626 "content": [{
627 "type": "text",
628 "text": serde_json::to_string_pretty(&json!({
629 "graph": name,
630 "unit_count": stats.unit_count,
631 "edge_count": stats.edge_count,
632 "dimension": stats.dimension,
633 })).unwrap_or_default()
634 }]
635 }),
636 )
637 }
638
639 fn tool_list_units(&self, id: Value, args: &Value) -> JsonRpcResponse {
641 let (_, graph) = match self.resolve_graph(args) {
642 Ok(g) => g,
643 Err(e) => return JsonRpcResponse::error(id, e),
644 };
645
646 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
647 let unit_type_filter = match args.get("unit_type").and_then(|v| v.as_str()) {
648 Some(raw) => match Self::parse_unit_type(raw) {
649 Some(parsed) => Some(parsed),
650 None => {
651 return JsonRpcResponse::error(
652 id,
653 JsonRpcError::invalid_params(format!(
654 "Unknown unit_type '{}'. Expected one of: module, symbol, type, function, parameter, import, test, doc, config, pattern, trait, impl, macro.",
655 raw
656 )),
657 );
658 }
659 },
660 None => None,
661 };
662
663 let units: Vec<Value> = graph
664 .units()
665 .iter()
666 .filter(|u| {
667 if let Some(expected) = unit_type_filter {
668 u.unit_type == expected
669 } else {
670 true
671 }
672 })
673 .take(limit)
674 .map(|u| {
675 json!({
676 "id": u.id,
677 "name": u.name,
678 "type": u.unit_type.label(),
679 "file": u.file_path.display().to_string(),
680 })
681 })
682 .collect();
683
684 JsonRpcResponse::success(
685 id,
686 json!({
687 "content": [{
688 "type": "text",
689 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
690 }]
691 }),
692 )
693 }
694}
695
696impl Default for McpServer {
697 fn default() -> Self {
698 Self::new()
699 }
700}