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", "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", "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 = match args
462 .get("mode")
463 .and_then(|v| v.as_str())
464 .unwrap_or("prefix")
465 {
466 "exact" => MatchMode::Exact,
467 "prefix" => MatchMode::Prefix,
468 "contains" => MatchMode::Contains,
469 "fuzzy" => MatchMode::Fuzzy,
470 _ => MatchMode::Prefix,
471 };
472
473 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
474
475 let params = SymbolLookupParams {
476 name,
477 mode,
478 limit,
479 ..SymbolLookupParams::default()
480 };
481
482 match self.engine.symbol_lookup(graph, params) {
483 Ok(units) => {
484 let results: Vec<Value> = units
485 .iter()
486 .map(|u| {
487 json!({
488 "id": u.id,
489 "name": u.name,
490 "qualified_name": u.qualified_name,
491 "type": u.unit_type.label(),
492 "file": u.file_path.display().to_string(),
493 "language": u.language.name(),
494 "complexity": u.complexity,
495 })
496 })
497 .collect();
498 JsonRpcResponse::success(
499 id,
500 json!({
501 "content": [{
502 "type": "text",
503 "text": serde_json::to_string_pretty(&results).unwrap_or_default()
504 }]
505 }),
506 )
507 }
508 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
509 }
510 }
511
512 fn tool_impact_analysis(&self, id: Value, args: &Value) -> JsonRpcResponse {
514 let (_, graph) = match self.resolve_graph(args) {
515 Ok(g) => g,
516 Err(e) => return JsonRpcResponse::error(id, e),
517 };
518
519 let unit_id = match args.get("unit_id").and_then(|v| v.as_u64()) {
520 Some(uid) => uid,
521 None => {
522 return JsonRpcResponse::error(
523 id,
524 JsonRpcError::invalid_params("Missing 'unit_id' argument"),
525 );
526 }
527 };
528
529 let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as u32;
530 let edge_types = vec![
531 EdgeType::Calls,
532 EdgeType::Imports,
533 EdgeType::Inherits,
534 EdgeType::Implements,
535 EdgeType::UsesType,
536 EdgeType::FfiBinds,
537 EdgeType::References,
538 EdgeType::Returns,
539 EdgeType::ParamType,
540 EdgeType::Overrides,
541 EdgeType::Contains,
542 ];
543
544 let params = ImpactParams {
545 unit_id,
546 max_depth,
547 edge_types,
548 };
549
550 match self.engine.impact_analysis(graph, params) {
551 Ok(result) => {
552 let impacted: Vec<Value> = result
553 .impacted
554 .iter()
555 .map(|i| {
556 json!({
557 "unit_id": i.unit_id,
558 "depth": i.depth,
559 "risk_score": i.risk_score,
560 "has_tests": i.has_tests,
561 })
562 })
563 .collect();
564 JsonRpcResponse::success(
565 id,
566 json!({
567 "content": [{
568 "type": "text",
569 "text": serde_json::to_string_pretty(&json!({
570 "root_id": result.root_id,
571 "overall_risk": result.overall_risk,
572 "impacted_count": result.impacted.len(),
573 "impacted": impacted,
574 "recommendations": result.recommendations,
575 })).unwrap_or_default()
576 }]
577 }),
578 )
579 }
580 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
581 }
582 }
583
584 fn tool_graph_stats(&self, id: Value, args: &Value) -> JsonRpcResponse {
586 let (name, graph) = match self.resolve_graph(args) {
587 Ok(g) => g,
588 Err(e) => return JsonRpcResponse::error(id, e),
589 };
590
591 let stats = graph.stats();
592 JsonRpcResponse::success(
593 id,
594 json!({
595 "content": [{
596 "type": "text",
597 "text": serde_json::to_string_pretty(&json!({
598 "graph": name,
599 "unit_count": stats.unit_count,
600 "edge_count": stats.edge_count,
601 "dimension": stats.dimension,
602 })).unwrap_or_default()
603 }]
604 }),
605 )
606 }
607
608 fn tool_list_units(&self, id: Value, args: &Value) -> JsonRpcResponse {
610 let (_, graph) = match self.resolve_graph(args) {
611 Ok(g) => g,
612 Err(e) => return JsonRpcResponse::error(id, e),
613 };
614
615 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
616 let unit_type_filter = match args.get("unit_type").and_then(|v| v.as_str()) {
617 Some(raw) => match Self::parse_unit_type(raw) {
618 Some(parsed) => Some(parsed),
619 None => {
620 return JsonRpcResponse::error(
621 id,
622 JsonRpcError::invalid_params(format!(
623 "Unknown unit_type '{}'. Expected one of: module, symbol, type, function, parameter, import, test, doc, config, pattern, trait, impl, macro.",
624 raw
625 )),
626 );
627 }
628 },
629 None => None,
630 };
631
632 let units: Vec<Value> = graph
633 .units()
634 .iter()
635 .filter(|u| {
636 if let Some(expected) = unit_type_filter {
637 u.unit_type == expected
638 } else {
639 true
640 }
641 })
642 .take(limit)
643 .map(|u| {
644 json!({
645 "id": u.id,
646 "name": u.name,
647 "type": u.unit_type.label(),
648 "file": u.file_path.display().to_string(),
649 })
650 })
651 .collect();
652
653 JsonRpcResponse::success(
654 id,
655 json!({
656 "content": [{
657 "type": "text",
658 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
659 }]
660 }),
661 )
662 }
663}
664
665impl Default for McpServer {
666 fn default() -> Self {
667 Self::new()
668 }
669}