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;
14
15use super::protocol::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
16
17const SERVER_NAME: &str = "agentic-codebase";
19const SERVER_VERSION: &str = "0.1.0";
21const PROTOCOL_VERSION: &str = "2024-11-05";
23
24#[derive(Debug)]
29pub struct McpServer {
30 graphs: HashMap<String, CodeGraph>,
32 engine: QueryEngine,
34 initialized: bool,
36}
37
38impl McpServer {
39 pub fn new() -> Self {
41 Self {
42 graphs: HashMap::new(),
43 engine: QueryEngine::new(),
44 initialized: false,
45 }
46 }
47
48 pub fn load_graph(&mut self, name: String, graph: CodeGraph) {
50 self.graphs.insert(name, graph);
51 }
52
53 pub fn unload_graph(&mut self, name: &str) -> Option<CodeGraph> {
55 self.graphs.remove(name)
56 }
57
58 pub fn get_graph(&self, name: &str) -> Option<&CodeGraph> {
60 self.graphs.get(name)
61 }
62
63 pub fn graph_names(&self) -> Vec<&str> {
65 self.graphs.keys().map(|s| s.as_str()).collect()
66 }
67
68 pub fn is_initialized(&self) -> bool {
70 self.initialized
71 }
72
73 pub fn handle_raw(&mut self, raw: &str) -> String {
78 let response = match super::protocol::parse_request(raw) {
79 Ok(request) => self.handle_request(request),
80 Err(error_response) => error_response,
81 };
82 serde_json::to_string(&response).unwrap_or_else(|_| {
83 r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Serialization failed"}}"#
84 .to_string()
85 })
86 }
87
88 pub fn handle_request(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
90 let id = request.id.clone();
91 match request.method.as_str() {
92 "initialize" => self.handle_initialize(id, &request.params),
93 "shutdown" => self.handle_shutdown(id),
94 "tools/list" => self.handle_tools_list(id),
95 "tools/call" => self.handle_tools_call(id, &request.params),
96 "resources/list" => self.handle_resources_list(id),
97 "resources/read" => self.handle_resources_read(id, &request.params),
98 "prompts/list" => self.handle_prompts_list(id),
99 _ => JsonRpcResponse::error(id, JsonRpcError::method_not_found(&request.method)),
100 }
101 }
102
103 fn handle_initialize(&mut self, id: Value, _params: &Value) -> JsonRpcResponse {
109 self.initialized = true;
110 JsonRpcResponse::success(
111 id,
112 json!({
113 "protocolVersion": PROTOCOL_VERSION,
114 "capabilities": {
115 "tools": { "listChanged": false },
116 "resources": { "subscribe": false, "listChanged": false },
117 "prompts": { "listChanged": false }
118 },
119 "serverInfo": {
120 "name": SERVER_NAME,
121 "version": SERVER_VERSION
122 }
123 }),
124 )
125 }
126
127 fn handle_shutdown(&mut self, id: Value) -> JsonRpcResponse {
129 self.initialized = false;
130 JsonRpcResponse::success(id, json!(null))
131 }
132
133 fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
135 JsonRpcResponse::success(
136 id,
137 json!({
138 "tools": [
139 {
140 "name": "symbol_lookup",
141 "description": "Look up symbols by name in the code graph.",
142 "inputSchema": {
143 "type": "object",
144 "properties": {
145 "graph": { "type": "string", "description": "Graph name" },
146 "name": { "type": "string", "description": "Symbol name to search for" },
147 "mode": { "type": "string", "enum": ["exact", "prefix", "contains", "fuzzy"], "default": "prefix" },
148 "limit": { "type": "integer", "default": 10 }
149 },
150 "required": ["name"]
151 }
152 },
153 {
154 "name": "impact_analysis",
155 "description": "Analyse the impact of changing a code unit.",
156 "inputSchema": {
157 "type": "object",
158 "properties": {
159 "graph": { "type": "string", "description": "Graph name" },
160 "unit_id": { "type": "integer", "description": "Code unit ID to analyse" },
161 "max_depth": { "type": "integer", "default": 3 }
162 },
163 "required": ["unit_id"]
164 }
165 },
166 {
167 "name": "graph_stats",
168 "description": "Get summary statistics about a loaded code graph.",
169 "inputSchema": {
170 "type": "object",
171 "properties": {
172 "graph": { "type": "string", "description": "Graph name" }
173 }
174 }
175 },
176 {
177 "name": "list_units",
178 "description": "List code units in a graph, optionally filtered by type.",
179 "inputSchema": {
180 "type": "object",
181 "properties": {
182 "graph": { "type": "string", "description": "Graph name" },
183 "unit_type": { "type": "string", "description": "Filter by unit type" },
184 "limit": { "type": "integer", "default": 50 }
185 }
186 }
187 }
188 ]
189 }),
190 )
191 }
192
193 fn handle_tools_call(&self, id: Value, params: &Value) -> JsonRpcResponse {
195 let tool_name = match params.get("name").and_then(|v| v.as_str()) {
196 Some(name) => name,
197 None => {
198 return JsonRpcResponse::error(
199 id,
200 JsonRpcError::invalid_params("Missing 'name' field in tools/call params"),
201 );
202 }
203 };
204
205 let arguments = params
206 .get("arguments")
207 .cloned()
208 .unwrap_or(Value::Object(serde_json::Map::new()));
209
210 match tool_name {
211 "symbol_lookup" => self.tool_symbol_lookup(id, &arguments),
212 "impact_analysis" => self.tool_impact_analysis(id, &arguments),
213 "graph_stats" => self.tool_graph_stats(id, &arguments),
214 "list_units" => self.tool_list_units(id, &arguments),
215 _ => JsonRpcResponse::error(
216 id,
217 JsonRpcError::method_not_found(format!("Unknown tool: {}", tool_name)),
218 ),
219 }
220 }
221
222 fn handle_resources_list(&self, id: Value) -> JsonRpcResponse {
224 let mut resources = Vec::new();
225
226 for name in self.graphs.keys() {
227 resources.push(json!({
228 "uri": format!("acb://graphs/{}/stats", name),
229 "name": format!("{} statistics", name),
230 "description": format!("Statistics for the {} code graph.", name),
231 "mimeType": "application/json"
232 }));
233 resources.push(json!({
234 "uri": format!("acb://graphs/{}/units", name),
235 "name": format!("{} units", name),
236 "description": format!("All code units in the {} graph.", name),
237 "mimeType": "application/json"
238 }));
239 }
240
241 JsonRpcResponse::success(id, json!({ "resources": resources }))
242 }
243
244 fn handle_resources_read(&self, id: Value, params: &Value) -> JsonRpcResponse {
246 let uri = match params.get("uri").and_then(|v| v.as_str()) {
247 Some(u) => u,
248 None => {
249 return JsonRpcResponse::error(
250 id,
251 JsonRpcError::invalid_params("Missing 'uri' field"),
252 );
253 }
254 };
255
256 if let Some(rest) = uri.strip_prefix("acb://graphs/") {
258 let parts: Vec<&str> = rest.splitn(2, '/').collect();
259 if parts.len() == 2 {
260 let graph_name = parts[0];
261 let resource = parts[1];
262
263 if let Some(graph) = self.graphs.get(graph_name) {
264 return match resource {
265 "stats" => {
266 let stats = graph.stats();
267 JsonRpcResponse::success(
268 id,
269 json!({
270 "contents": [{
271 "uri": uri,
272 "mimeType": "application/json",
273 "text": serde_json::to_string_pretty(&json!({
274 "unit_count": stats.unit_count,
275 "edge_count": stats.edge_count,
276 "dimension": stats.dimension,
277 })).unwrap_or_default()
278 }]
279 }),
280 )
281 }
282 "units" => {
283 let units: Vec<Value> = graph
284 .units()
285 .iter()
286 .map(|u| {
287 json!({
288 "id": u.id,
289 "name": u.name,
290 "type": u.unit_type.label(),
291 "file": u.file_path.display().to_string(),
292 })
293 })
294 .collect();
295 JsonRpcResponse::success(
296 id,
297 json!({
298 "contents": [{
299 "uri": uri,
300 "mimeType": "application/json",
301 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
302 }]
303 }),
304 )
305 }
306 _ => JsonRpcResponse::error(
307 id,
308 JsonRpcError::invalid_params(format!(
309 "Unknown resource type: {}",
310 resource
311 )),
312 ),
313 };
314 } else {
315 return JsonRpcResponse::error(
316 id,
317 JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)),
318 );
319 }
320 }
321 }
322
323 JsonRpcResponse::error(
324 id,
325 JsonRpcError::invalid_params(format!("Invalid resource URI: {}", uri)),
326 )
327 }
328
329 fn handle_prompts_list(&self, id: Value) -> JsonRpcResponse {
331 JsonRpcResponse::success(
332 id,
333 json!({
334 "prompts": [
335 {
336 "name": "analyse_unit",
337 "description": "Analyse a code unit including its dependencies, stability, and test coverage.",
338 "arguments": [
339 {
340 "name": "graph",
341 "description": "Graph name",
342 "required": false
343 },
344 {
345 "name": "unit_name",
346 "description": "Name of the code unit to analyse",
347 "required": true
348 }
349 ]
350 },
351 {
352 "name": "explain_coupling",
353 "description": "Explain coupling between two code units.",
354 "arguments": [
355 {
356 "name": "graph",
357 "description": "Graph name",
358 "required": false
359 },
360 {
361 "name": "unit_a",
362 "description": "First unit name",
363 "required": true
364 },
365 {
366 "name": "unit_b",
367 "description": "Second unit name",
368 "required": true
369 }
370 ]
371 }
372 ]
373 }),
374 )
375 }
376
377 fn resolve_graph<'a>(
383 &'a self,
384 args: &'a Value,
385 ) -> Result<(&'a str, &'a CodeGraph), JsonRpcError> {
386 let graph_name = args.get("graph").and_then(|v| v.as_str()).unwrap_or("");
387
388 if graph_name.is_empty() {
389 if let Some((name, graph)) = self.graphs.iter().next() {
391 return Ok((name.as_str(), graph));
392 }
393 return Err(JsonRpcError::invalid_params("No graphs loaded"));
394 }
395
396 self.graphs
397 .get(graph_name)
398 .map(|g| (graph_name, g))
399 .ok_or_else(|| JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)))
400 }
401
402 fn tool_symbol_lookup(&self, id: Value, args: &Value) -> JsonRpcResponse {
404 let (_, graph) = match self.resolve_graph(args) {
405 Ok(g) => g,
406 Err(e) => return JsonRpcResponse::error(id, e),
407 };
408
409 let name = match args.get("name").and_then(|v| v.as_str()) {
410 Some(n) => n.to_string(),
411 None => {
412 return JsonRpcResponse::error(
413 id,
414 JsonRpcError::invalid_params("Missing 'name' argument"),
415 );
416 }
417 };
418
419 let mode = match args
420 .get("mode")
421 .and_then(|v| v.as_str())
422 .unwrap_or("prefix")
423 {
424 "exact" => MatchMode::Exact,
425 "prefix" => MatchMode::Prefix,
426 "contains" => MatchMode::Contains,
427 "fuzzy" => MatchMode::Fuzzy,
428 _ => MatchMode::Prefix,
429 };
430
431 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
432
433 let params = SymbolLookupParams {
434 name,
435 mode,
436 limit,
437 ..SymbolLookupParams::default()
438 };
439
440 match self.engine.symbol_lookup(graph, params) {
441 Ok(units) => {
442 let results: Vec<Value> = units
443 .iter()
444 .map(|u| {
445 json!({
446 "id": u.id,
447 "name": u.name,
448 "qualified_name": u.qualified_name,
449 "type": u.unit_type.label(),
450 "file": u.file_path.display().to_string(),
451 "language": u.language.name(),
452 "complexity": u.complexity,
453 })
454 })
455 .collect();
456 JsonRpcResponse::success(
457 id,
458 json!({
459 "content": [{
460 "type": "text",
461 "text": serde_json::to_string_pretty(&results).unwrap_or_default()
462 }]
463 }),
464 )
465 }
466 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
467 }
468 }
469
470 fn tool_impact_analysis(&self, id: Value, args: &Value) -> JsonRpcResponse {
472 let (_, graph) = match self.resolve_graph(args) {
473 Ok(g) => g,
474 Err(e) => return JsonRpcResponse::error(id, e),
475 };
476
477 let unit_id = match args.get("unit_id").and_then(|v| v.as_u64()) {
478 Some(uid) => uid,
479 None => {
480 return JsonRpcResponse::error(
481 id,
482 JsonRpcError::invalid_params("Missing 'unit_id' argument"),
483 );
484 }
485 };
486
487 let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as u32;
488
489 let params = ImpactParams {
490 unit_id,
491 max_depth,
492 edge_types: Vec::new(),
493 };
494
495 match self.engine.impact_analysis(graph, params) {
496 Ok(result) => {
497 let impacted: Vec<Value> = result
498 .impacted
499 .iter()
500 .map(|i| {
501 json!({
502 "unit_id": i.unit_id,
503 "depth": i.depth,
504 "risk_score": i.risk_score,
505 "has_tests": i.has_tests,
506 })
507 })
508 .collect();
509 JsonRpcResponse::success(
510 id,
511 json!({
512 "content": [{
513 "type": "text",
514 "text": serde_json::to_string_pretty(&json!({
515 "root_id": result.root_id,
516 "overall_risk": result.overall_risk,
517 "impacted_count": result.impacted.len(),
518 "impacted": impacted,
519 "recommendations": result.recommendations,
520 })).unwrap_or_default()
521 }]
522 }),
523 )
524 }
525 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
526 }
527 }
528
529 fn tool_graph_stats(&self, id: Value, args: &Value) -> JsonRpcResponse {
531 let (name, graph) = match self.resolve_graph(args) {
532 Ok(g) => g,
533 Err(e) => return JsonRpcResponse::error(id, e),
534 };
535
536 let stats = graph.stats();
537 JsonRpcResponse::success(
538 id,
539 json!({
540 "content": [{
541 "type": "text",
542 "text": serde_json::to_string_pretty(&json!({
543 "graph": name,
544 "unit_count": stats.unit_count,
545 "edge_count": stats.edge_count,
546 "dimension": stats.dimension,
547 })).unwrap_or_default()
548 }]
549 }),
550 )
551 }
552
553 fn tool_list_units(&self, id: Value, args: &Value) -> JsonRpcResponse {
555 let (_, graph) = match self.resolve_graph(args) {
556 Ok(g) => g,
557 Err(e) => return JsonRpcResponse::error(id, e),
558 };
559
560 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
561
562 let units: Vec<Value> = graph
563 .units()
564 .iter()
565 .take(limit)
566 .map(|u| {
567 json!({
568 "id": u.id,
569 "name": u.name,
570 "type": u.unit_type.label(),
571 "file": u.file_path.display().to_string(),
572 })
573 })
574 .collect();
575
576 JsonRpcResponse::success(
577 id,
578 json!({
579 "content": [{
580 "type": "text",
581 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
582 }]
583 }),
584 )
585 }
586}
587
588impl Default for McpServer {
589 fn default() -> Self {
590 Self::new()
591 }
592}