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