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::grounding::{Grounded, GroundingEngine, GroundingResult};
15use crate::types::{CodeUnitType, EdgeType};
16use crate::workspace::{ContextRole, TranslationMap, TranslationStatus, WorkspaceManager};
17
18use super::protocol::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
19
20const SERVER_NAME: &str = "agentic-codebase";
22const SERVER_VERSION: &str = "0.1.0";
24const PROTOCOL_VERSION: &str = "2024-11-05";
26
27#[derive(Debug, Clone)]
29pub struct OperationRecord {
30 pub tool_name: String,
31 pub summary: String,
32 pub timestamp: u64,
33 pub graph_name: Option<String>,
34}
35
36#[derive(Debug)]
41pub struct McpServer {
42 graphs: HashMap<String, CodeGraph>,
44 engine: QueryEngine,
46 initialized: bool,
48 operation_log: Vec<OperationRecord>,
50 session_start_time: Option<u64>,
52 workspace_manager: WorkspaceManager,
54 translation_maps: HashMap<String, TranslationMap>,
56}
57
58impl McpServer {
59 fn parse_unit_type(raw: &str) -> Option<CodeUnitType> {
60 match raw.trim().to_ascii_lowercase().as_str() {
61 "module" | "modules" => Some(CodeUnitType::Module),
62 "symbol" | "symbols" => Some(CodeUnitType::Symbol),
63 "type" | "types" => Some(CodeUnitType::Type),
64 "function" | "functions" => Some(CodeUnitType::Function),
65 "parameter" | "parameters" => Some(CodeUnitType::Parameter),
66 "import" | "imports" => Some(CodeUnitType::Import),
67 "test" | "tests" => Some(CodeUnitType::Test),
68 "doc" | "docs" | "document" | "documents" => Some(CodeUnitType::Doc),
69 "config" | "configs" => Some(CodeUnitType::Config),
70 "pattern" | "patterns" => Some(CodeUnitType::Pattern),
71 "trait" | "traits" => Some(CodeUnitType::Trait),
72 "impl" | "implementation" | "implementations" => Some(CodeUnitType::Impl),
73 "macro" | "macros" => Some(CodeUnitType::Macro),
74 _ => None,
75 }
76 }
77
78 pub fn new() -> Self {
80 Self {
81 graphs: HashMap::new(),
82 engine: QueryEngine::new(),
83 initialized: false,
84 operation_log: Vec::new(),
85 session_start_time: None,
86 workspace_manager: WorkspaceManager::new(),
87 translation_maps: HashMap::new(),
88 }
89 }
90
91 pub fn load_graph(&mut self, name: String, graph: CodeGraph) {
93 self.graphs.insert(name, graph);
94 }
95
96 pub fn unload_graph(&mut self, name: &str) -> Option<CodeGraph> {
98 self.graphs.remove(name)
99 }
100
101 pub fn get_graph(&self, name: &str) -> Option<&CodeGraph> {
103 self.graphs.get(name)
104 }
105
106 pub fn graph_names(&self) -> Vec<&str> {
108 self.graphs.keys().map(|s| s.as_str()).collect()
109 }
110
111 pub fn is_initialized(&self) -> bool {
113 self.initialized
114 }
115
116 pub fn handle_raw(&mut self, raw: &str) -> String {
121 let response = match super::protocol::parse_request(raw) {
122 Ok(request) => {
123 if request.id.is_none() {
124 self.handle_notification(&request.method, &request.params);
125 return String::new();
126 }
127 self.handle_request(request)
128 }
129 Err(error_response) => error_response,
130 };
131 serde_json::to_string(&response).unwrap_or_else(|_| {
132 r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Serialization failed"}}"#
133 .to_string()
134 })
135 }
136
137 pub fn handle_request(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
139 let id = request.id.clone().unwrap_or(Value::Null);
140 match request.method.as_str() {
141 "initialize" => self.handle_initialize(id, &request.params),
142 "shutdown" => self.handle_shutdown(id),
143 "tools/list" => self.handle_tools_list(id),
144 "tools/call" => self.handle_tools_call(id, &request.params),
145 "resources/list" => self.handle_resources_list(id),
146 "resources/read" => self.handle_resources_read(id, &request.params),
147 "prompts/list" => self.handle_prompts_list(id),
148 _ => JsonRpcResponse::error(id, JsonRpcError::method_not_found(&request.method)),
149 }
150 }
151
152 fn handle_notification(&mut self, method: &str, _params: &Value) {
156 if method == "notifications/initialized" {
157 self.initialized = true;
158 }
159 }
160
161 fn handle_initialize(&mut self, id: Value, _params: &Value) -> JsonRpcResponse {
167 self.initialized = true;
168 self.session_start_time = Some(
169 std::time::SystemTime::now()
170 .duration_since(std::time::UNIX_EPOCH)
171 .unwrap_or_default()
172 .as_secs(),
173 );
174 self.operation_log.clear();
175 JsonRpcResponse::success(
176 id,
177 json!({
178 "protocolVersion": PROTOCOL_VERSION,
179 "capabilities": {
180 "tools": { "listChanged": false },
181 "resources": { "subscribe": false, "listChanged": false },
182 "prompts": { "listChanged": false }
183 },
184 "serverInfo": {
185 "name": SERVER_NAME,
186 "version": SERVER_VERSION
187 }
188 }),
189 )
190 }
191
192 fn handle_shutdown(&mut self, id: Value) -> JsonRpcResponse {
194 self.initialized = false;
195 JsonRpcResponse::success(id, json!(null))
196 }
197
198 fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
200 JsonRpcResponse::success(
201 id,
202 json!({
203 "tools": [
204 {
205 "name": "symbol_lookup",
206 "description": "Look up symbols by name in the code graph.",
207 "inputSchema": {
208 "type": "object",
209 "properties": {
210 "graph": { "type": "string", "description": "Graph name" },
211 "name": { "type": "string", "description": "Symbol name to search for" },
212 "mode": { "type": "string", "enum": ["exact", "prefix", "contains", "fuzzy"], "default": "prefix" },
213 "limit": { "type": "integer", "minimum": 1, "default": 10 }
214 },
215 "required": ["name"]
216 }
217 },
218 {
219 "name": "impact_analysis",
220 "description": "Analyse the impact of changing a code unit.",
221 "inputSchema": {
222 "type": "object",
223 "properties": {
224 "graph": { "type": "string", "description": "Graph name" },
225 "unit_id": { "type": "integer", "description": "Code unit ID to analyse" },
226 "max_depth": { "type": "integer", "minimum": 0, "default": 3 }
227 },
228 "required": ["unit_id"]
229 }
230 },
231 {
232 "name": "graph_stats",
233 "description": "Get summary statistics about a loaded code graph.",
234 "inputSchema": {
235 "type": "object",
236 "properties": {
237 "graph": { "type": "string", "description": "Graph name" }
238 }
239 }
240 },
241 {
242 "name": "list_units",
243 "description": "List code units in a graph, optionally filtered by type.",
244 "inputSchema": {
245 "type": "object",
246 "properties": {
247 "graph": { "type": "string", "description": "Graph name" },
248 "unit_type": {
249 "type": "string",
250 "description": "Filter by unit type",
251 "enum": [
252 "module", "symbol", "type", "function", "parameter", "import",
253 "test", "doc", "config", "pattern", "trait", "impl", "macro"
254 ]
255 },
256 "limit": { "type": "integer", "default": 50 }
257 }
258 }
259 },
260 {
261 "name": "analysis_log",
262 "description": "Log the intent and context behind a code analysis. Call this to record WHY you are performing a lookup or analysis.",
263 "inputSchema": {
264 "type": "object",
265 "properties": {
266 "intent": {
267 "type": "string",
268 "description": "Why you are analysing — the goal or reason for the code query"
269 },
270 "finding": {
271 "type": "string",
272 "description": "What you found or concluded from the analysis"
273 },
274 "graph": {
275 "type": "string",
276 "description": "Optional graph name this analysis relates to"
277 },
278 "topic": {
279 "type": "string",
280 "description": "Optional topic or category (e.g., 'refactoring', 'bug-hunt')"
281 }
282 },
283 "required": ["intent"]
284 }
285 },
286 {
288 "name": "codebase_ground",
289 "description": "Verify a claim about code has graph evidence. Use before asserting code exists.",
290 "inputSchema": {
291 "type": "object",
292 "properties": {
293 "claim": { "type": "string", "description": "The claim to verify (e.g., 'function validate_token exists')" },
294 "graph": { "type": "string", "description": "Graph name" },
295 "strict": { "type": "boolean", "description": "If true, partial matches return Ungrounded (default: false)", "default": false }
296 },
297 "required": ["claim"]
298 }
299 },
300 {
301 "name": "codebase_evidence",
302 "description": "Get graph evidence for a symbol name.",
303 "inputSchema": {
304 "type": "object",
305 "properties": {
306 "name": { "type": "string", "description": "Symbol name to find" },
307 "graph": { "type": "string", "description": "Graph name" },
308 "types": {
309 "type": "array",
310 "items": { "type": "string" },
311 "description": "Filter by type: function, struct, enum, module, trait (optional)"
312 }
313 },
314 "required": ["name"]
315 }
316 },
317 {
318 "name": "codebase_suggest",
319 "description": "Find symbols similar to a name (for corrections).",
320 "inputSchema": {
321 "type": "object",
322 "properties": {
323 "name": { "type": "string", "description": "Name to find similar matches for" },
324 "graph": { "type": "string", "description": "Graph name" },
325 "limit": { "type": "integer", "minimum": 1, "default": 5, "description": "Max suggestions (default: 5)" }
326 },
327 "required": ["name"]
328 }
329 },
330 {
332 "name": "workspace_create",
333 "description": "Create a workspace to load multiple codebases.",
334 "inputSchema": {
335 "type": "object",
336 "properties": {
337 "name": { "type": "string", "description": "Workspace name (e.g., 'cpp-to-rust-migration')" }
338 },
339 "required": ["name"]
340 }
341 },
342 {
343 "name": "workspace_add",
344 "description": "Add a codebase to an existing workspace.",
345 "inputSchema": {
346 "type": "object",
347 "properties": {
348 "workspace": { "type": "string", "description": "Workspace name or id" },
349 "graph": { "type": "string", "description": "Name of a loaded graph to add" },
350 "path": { "type": "string", "description": "Path label for this codebase" },
351 "role": { "type": "string", "enum": ["source", "target", "reference", "comparison"], "description": "Role of this codebase" },
352 "language": { "type": "string", "description": "Optional language hint" }
353 },
354 "required": ["workspace", "graph", "role"]
355 }
356 },
357 {
358 "name": "workspace_list",
359 "description": "List all contexts in a workspace.",
360 "inputSchema": {
361 "type": "object",
362 "properties": {
363 "workspace": { "type": "string", "description": "Workspace name or id" }
364 },
365 "required": ["workspace"]
366 }
367 },
368 {
369 "name": "workspace_query",
370 "description": "Search across all codebases in workspace.",
371 "inputSchema": {
372 "type": "object",
373 "properties": {
374 "workspace": { "type": "string", "description": "Workspace name or id" },
375 "query": { "type": "string", "description": "Search query" },
376 "roles": { "type": "array", "items": { "type": "string" }, "description": "Filter by role (optional)" }
377 },
378 "required": ["workspace", "query"]
379 }
380 },
381 {
382 "name": "workspace_compare",
383 "description": "Compare a symbol between source and target.",
384 "inputSchema": {
385 "type": "object",
386 "properties": {
387 "workspace": { "type": "string", "description": "Workspace name or id" },
388 "symbol": { "type": "string", "description": "Symbol to compare" }
389 },
390 "required": ["workspace", "symbol"]
391 }
392 },
393 {
394 "name": "workspace_xref",
395 "description": "Find where symbol exists/doesn't exist across contexts.",
396 "inputSchema": {
397 "type": "object",
398 "properties": {
399 "workspace": { "type": "string", "description": "Workspace name or id" },
400 "symbol": { "type": "string", "description": "Symbol to find" }
401 },
402 "required": ["workspace", "symbol"]
403 }
404 },
405 {
407 "name": "translation_record",
408 "description": "Record source→target symbol mapping.",
409 "inputSchema": {
410 "type": "object",
411 "properties": {
412 "workspace": { "type": "string", "description": "Workspace name or id" },
413 "source_symbol": { "type": "string", "description": "Symbol in source codebase" },
414 "target_symbol": { "type": "string", "description": "Symbol in target (null if not ported)" },
415 "status": { "type": "string", "enum": ["not_started", "in_progress", "ported", "verified", "skipped"], "description": "Porting status" },
416 "notes": { "type": "string", "description": "Optional notes" }
417 },
418 "required": ["workspace", "source_symbol", "status"]
419 }
420 },
421 {
422 "name": "translation_progress",
423 "description": "Get migration progress statistics.",
424 "inputSchema": {
425 "type": "object",
426 "properties": {
427 "workspace": { "type": "string", "description": "Workspace name or id" }
428 },
429 "required": ["workspace"]
430 }
431 },
432 {
433 "name": "translation_remaining",
434 "description": "List symbols not yet ported.",
435 "inputSchema": {
436 "type": "object",
437 "properties": {
438 "workspace": { "type": "string", "description": "Workspace name or id" },
439 "module": { "type": "string", "description": "Filter by module (optional)" }
440 },
441 "required": ["workspace"]
442 }
443 }
444 ]
445 }),
446 )
447 }
448
449 fn handle_tools_call(&mut self, id: Value, params: &Value) -> JsonRpcResponse {
451 let tool_name = match params.get("name").and_then(|v| v.as_str()) {
452 Some(name) => name,
453 None => {
454 return JsonRpcResponse::error(
455 id,
456 JsonRpcError::invalid_params("Missing 'name' field in tools/call params"),
457 );
458 }
459 };
460
461 let arguments = params
462 .get("arguments")
463 .cloned()
464 .unwrap_or(Value::Object(serde_json::Map::new()));
465
466 let result = match tool_name {
467 "symbol_lookup" => self.tool_symbol_lookup(id.clone(), &arguments),
468 "impact_analysis" => self.tool_impact_analysis(id.clone(), &arguments),
469 "graph_stats" => self.tool_graph_stats(id.clone(), &arguments),
470 "list_units" => self.tool_list_units(id.clone(), &arguments),
471 "analysis_log" => return self.tool_analysis_log(id, &arguments),
472 "codebase_ground" => self.tool_codebase_ground(id.clone(), &arguments),
474 "codebase_evidence" => self.tool_codebase_evidence(id.clone(), &arguments),
475 "codebase_suggest" => self.tool_codebase_suggest(id.clone(), &arguments),
476 "workspace_create" => return self.tool_workspace_create(id, &arguments),
478 "workspace_add" => return self.tool_workspace_add(id, &arguments),
479 "workspace_list" => self.tool_workspace_list(id.clone(), &arguments),
480 "workspace_query" => self.tool_workspace_query(id.clone(), &arguments),
481 "workspace_compare" => self.tool_workspace_compare(id.clone(), &arguments),
482 "workspace_xref" => self.tool_workspace_xref(id.clone(), &arguments),
483 "translation_record" => return self.tool_translation_record(id, &arguments),
485 "translation_progress" => self.tool_translation_progress(id.clone(), &arguments),
486 "translation_remaining" => self.tool_translation_remaining(id.clone(), &arguments),
487 _ => {
488 return JsonRpcResponse::error(
489 id,
490 JsonRpcError::method_not_found(format!("Unknown tool: {}", tool_name)),
491 );
492 }
493 };
494
495 let now = std::time::SystemTime::now()
497 .duration_since(std::time::UNIX_EPOCH)
498 .unwrap_or_default()
499 .as_secs();
500 let summary = truncate_json_summary(&arguments, 200);
501 let graph_name = arguments
502 .get("graph")
503 .and_then(|v| v.as_str())
504 .map(String::from);
505 self.operation_log.push(OperationRecord {
506 tool_name: tool_name.to_string(),
507 summary,
508 timestamp: now,
509 graph_name,
510 });
511
512 result
513 }
514
515 fn handle_resources_list(&self, id: Value) -> JsonRpcResponse {
517 let mut resources = Vec::new();
518
519 for name in self.graphs.keys() {
520 resources.push(json!({
521 "uri": format!("acb://graphs/{}/stats", name),
522 "name": format!("{} statistics", name),
523 "description": format!("Statistics for the {} code graph.", name),
524 "mimeType": "application/json"
525 }));
526 resources.push(json!({
527 "uri": format!("acb://graphs/{}/units", name),
528 "name": format!("{} units", name),
529 "description": format!("All code units in the {} graph.", name),
530 "mimeType": "application/json"
531 }));
532 }
533
534 JsonRpcResponse::success(id, json!({ "resources": resources }))
535 }
536
537 fn handle_resources_read(&self, id: Value, params: &Value) -> JsonRpcResponse {
539 let uri = match params.get("uri").and_then(|v| v.as_str()) {
540 Some(u) => u,
541 None => {
542 return JsonRpcResponse::error(
543 id,
544 JsonRpcError::invalid_params("Missing 'uri' field"),
545 );
546 }
547 };
548
549 if let Some(rest) = uri.strip_prefix("acb://graphs/") {
551 let parts: Vec<&str> = rest.splitn(2, '/').collect();
552 if parts.len() == 2 {
553 let graph_name = parts[0];
554 let resource = parts[1];
555
556 if let Some(graph) = self.graphs.get(graph_name) {
557 return match resource {
558 "stats" => {
559 let stats = graph.stats();
560 JsonRpcResponse::success(
561 id,
562 json!({
563 "contents": [{
564 "uri": uri,
565 "mimeType": "application/json",
566 "text": serde_json::to_string_pretty(&json!({
567 "unit_count": stats.unit_count,
568 "edge_count": stats.edge_count,
569 "dimension": stats.dimension,
570 })).unwrap_or_default()
571 }]
572 }),
573 )
574 }
575 "units" => {
576 let units: Vec<Value> = graph
577 .units()
578 .iter()
579 .map(|u| {
580 json!({
581 "id": u.id,
582 "name": u.name,
583 "type": u.unit_type.label(),
584 "file": u.file_path.display().to_string(),
585 })
586 })
587 .collect();
588 JsonRpcResponse::success(
589 id,
590 json!({
591 "contents": [{
592 "uri": uri,
593 "mimeType": "application/json",
594 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
595 }]
596 }),
597 )
598 }
599 _ => JsonRpcResponse::error(
600 id,
601 JsonRpcError::invalid_params(format!(
602 "Unknown resource type: {}",
603 resource
604 )),
605 ),
606 };
607 } else {
608 return JsonRpcResponse::error(
609 id,
610 JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)),
611 );
612 }
613 }
614 }
615
616 JsonRpcResponse::error(
617 id,
618 JsonRpcError::invalid_params(format!("Invalid resource URI: {}", uri)),
619 )
620 }
621
622 fn handle_prompts_list(&self, id: Value) -> JsonRpcResponse {
624 JsonRpcResponse::success(
625 id,
626 json!({
627 "prompts": [
628 {
629 "name": "analyse_unit",
630 "description": "Analyse a code unit including its dependencies, stability, and test coverage.",
631 "arguments": [
632 {
633 "name": "graph",
634 "description": "Graph name",
635 "required": false
636 },
637 {
638 "name": "unit_name",
639 "description": "Name of the code unit to analyse",
640 "required": true
641 }
642 ]
643 },
644 {
645 "name": "explain_coupling",
646 "description": "Explain coupling between two code units.",
647 "arguments": [
648 {
649 "name": "graph",
650 "description": "Graph name",
651 "required": false
652 },
653 {
654 "name": "unit_a",
655 "description": "First unit name",
656 "required": true
657 },
658 {
659 "name": "unit_b",
660 "description": "Second unit name",
661 "required": true
662 }
663 ]
664 }
665 ]
666 }),
667 )
668 }
669
670 fn resolve_graph<'a>(
676 &'a self,
677 args: &'a Value,
678 ) -> Result<(&'a str, &'a CodeGraph), JsonRpcError> {
679 let graph_name = args.get("graph").and_then(|v| v.as_str()).unwrap_or("");
680
681 if graph_name.is_empty() {
682 if let Some((name, graph)) = self.graphs.iter().next() {
684 return Ok((name.as_str(), graph));
685 }
686 return Err(JsonRpcError::invalid_params("No graphs loaded"));
687 }
688
689 self.graphs
690 .get(graph_name)
691 .map(|g| (graph_name, g))
692 .ok_or_else(|| JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)))
693 }
694
695 fn tool_symbol_lookup(&self, id: Value, args: &Value) -> JsonRpcResponse {
697 let (_, graph) = match self.resolve_graph(args) {
698 Ok(g) => g,
699 Err(e) => return JsonRpcResponse::error(id, e),
700 };
701
702 let name = match args.get("name").and_then(|v| v.as_str()) {
703 Some(n) => n.to_string(),
704 None => {
705 return JsonRpcResponse::error(
706 id,
707 JsonRpcError::invalid_params("Missing 'name' argument"),
708 );
709 }
710 };
711
712 let mode_raw = args
713 .get("mode")
714 .and_then(|v| v.as_str())
715 .unwrap_or("prefix");
716 let mode = match mode_raw {
717 "exact" => MatchMode::Exact,
718 "prefix" => MatchMode::Prefix,
719 "contains" => MatchMode::Contains,
720 "fuzzy" => MatchMode::Fuzzy,
721 _ => {
722 return JsonRpcResponse::error(
723 id,
724 JsonRpcError::invalid_params(format!(
725 "Invalid 'mode': {mode_raw}. Expected one of: exact, prefix, contains, fuzzy"
726 )),
727 );
728 }
729 };
730
731 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
732
733 let params = SymbolLookupParams {
734 name,
735 mode,
736 limit,
737 ..SymbolLookupParams::default()
738 };
739
740 match self.engine.symbol_lookup(graph, params) {
741 Ok(units) => {
742 let results: Vec<Value> = units
743 .iter()
744 .map(|u| {
745 json!({
746 "id": u.id,
747 "name": u.name,
748 "qualified_name": u.qualified_name,
749 "type": u.unit_type.label(),
750 "file": u.file_path.display().to_string(),
751 "language": u.language.name(),
752 "complexity": u.complexity,
753 })
754 })
755 .collect();
756 JsonRpcResponse::success(
757 id,
758 json!({
759 "content": [{
760 "type": "text",
761 "text": serde_json::to_string_pretty(&results).unwrap_or_default()
762 }]
763 }),
764 )
765 }
766 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
767 }
768 }
769
770 fn tool_impact_analysis(&self, id: Value, args: &Value) -> JsonRpcResponse {
772 let (_, graph) = match self.resolve_graph(args) {
773 Ok(g) => g,
774 Err(e) => return JsonRpcResponse::error(id, e),
775 };
776
777 let unit_id = match args.get("unit_id").and_then(|v| v.as_u64()) {
778 Some(uid) => uid,
779 None => {
780 return JsonRpcResponse::error(
781 id,
782 JsonRpcError::invalid_params("Missing 'unit_id' argument"),
783 );
784 }
785 };
786
787 let max_depth = match args.get("max_depth") {
788 None => 3,
789 Some(v) => {
790 let depth = match v.as_i64() {
791 Some(d) => d,
792 None => {
793 return JsonRpcResponse::error(
794 id,
795 JsonRpcError::invalid_params("'max_depth' must be an integer >= 0"),
796 );
797 }
798 };
799 if depth < 0 {
800 return JsonRpcResponse::error(
801 id,
802 JsonRpcError::invalid_params("'max_depth' must be >= 0"),
803 );
804 }
805 depth as u32
806 }
807 };
808 let edge_types = vec![
809 EdgeType::Calls,
810 EdgeType::Imports,
811 EdgeType::Inherits,
812 EdgeType::Implements,
813 EdgeType::UsesType,
814 EdgeType::FfiBinds,
815 EdgeType::References,
816 EdgeType::Returns,
817 EdgeType::ParamType,
818 EdgeType::Overrides,
819 EdgeType::Contains,
820 ];
821
822 let params = ImpactParams {
823 unit_id,
824 max_depth,
825 edge_types,
826 };
827
828 match self.engine.impact_analysis(graph, params) {
829 Ok(result) => {
830 let impacted: Vec<Value> = result
831 .impacted
832 .iter()
833 .map(|i| {
834 json!({
835 "unit_id": i.unit_id,
836 "depth": i.depth,
837 "risk_score": i.risk_score,
838 "has_tests": i.has_tests,
839 })
840 })
841 .collect();
842 JsonRpcResponse::success(
843 id,
844 json!({
845 "content": [{
846 "type": "text",
847 "text": serde_json::to_string_pretty(&json!({
848 "root_id": result.root_id,
849 "overall_risk": result.overall_risk,
850 "impacted_count": result.impacted.len(),
851 "impacted": impacted,
852 "recommendations": result.recommendations,
853 })).unwrap_or_default()
854 }]
855 }),
856 )
857 }
858 Err(e) => JsonRpcResponse::error(id, JsonRpcError::internal_error(e.to_string())),
859 }
860 }
861
862 fn tool_graph_stats(&self, id: Value, args: &Value) -> JsonRpcResponse {
864 let (name, graph) = match self.resolve_graph(args) {
865 Ok(g) => g,
866 Err(e) => return JsonRpcResponse::error(id, e),
867 };
868
869 let stats = graph.stats();
870 JsonRpcResponse::success(
871 id,
872 json!({
873 "content": [{
874 "type": "text",
875 "text": serde_json::to_string_pretty(&json!({
876 "graph": name,
877 "unit_count": stats.unit_count,
878 "edge_count": stats.edge_count,
879 "dimension": stats.dimension,
880 })).unwrap_or_default()
881 }]
882 }),
883 )
884 }
885
886 fn tool_list_units(&self, id: Value, args: &Value) -> JsonRpcResponse {
888 let (_, graph) = match self.resolve_graph(args) {
889 Ok(g) => g,
890 Err(e) => return JsonRpcResponse::error(id, e),
891 };
892
893 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
894 let unit_type_filter = match args.get("unit_type").and_then(|v| v.as_str()) {
895 Some(raw) => match Self::parse_unit_type(raw) {
896 Some(parsed) => Some(parsed),
897 None => {
898 return JsonRpcResponse::error(
899 id,
900 JsonRpcError::invalid_params(format!(
901 "Unknown unit_type '{}'. Expected one of: module, symbol, type, function, parameter, import, test, doc, config, pattern, trait, impl, macro.",
902 raw
903 )),
904 );
905 }
906 },
907 None => None,
908 };
909
910 let units: Vec<Value> = graph
911 .units()
912 .iter()
913 .filter(|u| {
914 if let Some(expected) = unit_type_filter {
915 u.unit_type == expected
916 } else {
917 true
918 }
919 })
920 .take(limit)
921 .map(|u| {
922 json!({
923 "id": u.id,
924 "name": u.name,
925 "type": u.unit_type.label(),
926 "file": u.file_path.display().to_string(),
927 })
928 })
929 .collect();
930
931 JsonRpcResponse::success(
932 id,
933 json!({
934 "content": [{
935 "type": "text",
936 "text": serde_json::to_string_pretty(&units).unwrap_or_default()
937 }]
938 }),
939 )
940 }
941
942 fn tool_analysis_log(&mut self, id: Value, args: &Value) -> JsonRpcResponse {
944 let intent = match args.get("intent").and_then(|v| v.as_str()) {
945 Some(i) if !i.trim().is_empty() => i,
946 _ => {
947 return JsonRpcResponse::error(
948 id,
949 JsonRpcError::invalid_params("'intent' is required and must not be empty"),
950 );
951 }
952 };
953
954 let finding = args.get("finding").and_then(|v| v.as_str());
955 let graph_name = args.get("graph").and_then(|v| v.as_str());
956 let topic = args.get("topic").and_then(|v| v.as_str());
957
958 let now = std::time::SystemTime::now()
959 .duration_since(std::time::UNIX_EPOCH)
960 .unwrap_or_default()
961 .as_secs();
962
963 let mut summary_parts = vec![format!("intent: {intent}")];
964 if let Some(f) = finding {
965 summary_parts.push(format!("finding: {f}"));
966 }
967 if let Some(t) = topic {
968 summary_parts.push(format!("topic: {t}"));
969 }
970
971 let record = OperationRecord {
972 tool_name: "analysis_log".to_string(),
973 summary: summary_parts.join(" | "),
974 timestamp: now,
975 graph_name: graph_name.map(String::from),
976 };
977
978 let index = self.operation_log.len();
979 self.operation_log.push(record);
980
981 JsonRpcResponse::success(
982 id,
983 json!({
984 "content": [{
985 "type": "text",
986 "text": serde_json::to_string_pretty(&json!({
987 "log_index": index,
988 "message": "Analysis context logged"
989 })).unwrap_or_default()
990 }]
991 }),
992 )
993 }
994
995 pub fn operation_log(&self) -> &[OperationRecord] {
997 &self.operation_log
998 }
999
1000 pub fn workspace_manager(&self) -> &WorkspaceManager {
1002 &self.workspace_manager
1003 }
1004
1005 pub fn workspace_manager_mut(&mut self) -> &mut WorkspaceManager {
1007 &mut self.workspace_manager
1008 }
1009
1010 fn tool_codebase_ground(&self, id: Value, args: &Value) -> JsonRpcResponse {
1016 let (_, graph) = match self.resolve_graph(args) {
1017 Ok(g) => g,
1018 Err(e) => return JsonRpcResponse::error(id, e),
1019 };
1020
1021 let claim = match args.get("claim").and_then(|v| v.as_str()) {
1022 Some(c) if !c.trim().is_empty() => c,
1023 _ => {
1024 return JsonRpcResponse::error(
1025 id,
1026 JsonRpcError::invalid_params("Missing or empty 'claim' argument"),
1027 );
1028 }
1029 };
1030
1031 let strict = args
1032 .get("strict")
1033 .and_then(|v| v.as_bool())
1034 .unwrap_or(false);
1035
1036 let engine = GroundingEngine::new(graph);
1037 let result = engine.ground_claim(claim);
1038
1039 let result = if strict {
1041 match result {
1042 GroundingResult::Partial {
1043 unsupported,
1044 suggestions,
1045 ..
1046 } => GroundingResult::Ungrounded {
1047 claim: claim.to_string(),
1048 suggestions: {
1049 let mut s = unsupported;
1050 s.extend(suggestions);
1051 s
1052 },
1053 },
1054 other => other,
1055 }
1056 } else {
1057 result
1058 };
1059
1060 let output = match &result {
1061 GroundingResult::Verified {
1062 evidence,
1063 confidence,
1064 } => json!({
1065 "status": "verified",
1066 "confidence": confidence,
1067 "evidence": evidence.iter().map(|e| json!({
1068 "node_id": e.node_id,
1069 "node_type": e.node_type,
1070 "name": e.name,
1071 "file_path": e.file_path,
1072 "line_number": e.line_number,
1073 "snippet": e.snippet,
1074 })).collect::<Vec<_>>(),
1075 }),
1076 GroundingResult::Partial {
1077 supported,
1078 unsupported,
1079 suggestions,
1080 } => json!({
1081 "status": "partial",
1082 "supported": supported,
1083 "unsupported": unsupported,
1084 "suggestions": suggestions,
1085 }),
1086 GroundingResult::Ungrounded {
1087 claim, suggestions, ..
1088 } => json!({
1089 "status": "ungrounded",
1090 "claim": claim,
1091 "suggestions": suggestions,
1092 }),
1093 };
1094
1095 JsonRpcResponse::success(
1096 id,
1097 json!({
1098 "content": [{
1099 "type": "text",
1100 "text": serde_json::to_string_pretty(&output).unwrap_or_default()
1101 }]
1102 }),
1103 )
1104 }
1105
1106 fn tool_codebase_evidence(&self, id: Value, args: &Value) -> JsonRpcResponse {
1108 let (_, graph) = match self.resolve_graph(args) {
1109 Ok(g) => g,
1110 Err(e) => return JsonRpcResponse::error(id, e),
1111 };
1112
1113 let name = match args.get("name").and_then(|v| v.as_str()) {
1114 Some(n) if !n.trim().is_empty() => n,
1115 _ => {
1116 return JsonRpcResponse::error(
1117 id,
1118 JsonRpcError::invalid_params("Missing or empty 'name' argument"),
1119 );
1120 }
1121 };
1122
1123 let type_filters: Vec<String> = args
1124 .get("types")
1125 .and_then(|v| v.as_array())
1126 .map(|arr| {
1127 arr.iter()
1128 .filter_map(|v| v.as_str().map(|s| s.to_lowercase()))
1129 .collect()
1130 })
1131 .unwrap_or_default();
1132
1133 let engine = GroundingEngine::new(graph);
1134 let mut evidence = engine.find_evidence(name);
1135
1136 if !type_filters.is_empty() {
1138 evidence.retain(|e| type_filters.contains(&e.node_type.to_lowercase()));
1139 }
1140
1141 let output: Vec<Value> = evidence
1142 .iter()
1143 .map(|e| {
1144 json!({
1145 "node_id": e.node_id,
1146 "node_type": e.node_type,
1147 "name": e.name,
1148 "file_path": e.file_path,
1149 "line_number": e.line_number,
1150 "snippet": e.snippet,
1151 })
1152 })
1153 .collect();
1154
1155 JsonRpcResponse::success(
1156 id,
1157 json!({
1158 "content": [{
1159 "type": "text",
1160 "text": serde_json::to_string_pretty(&output).unwrap_or_default()
1161 }]
1162 }),
1163 )
1164 }
1165
1166 fn tool_codebase_suggest(&self, id: Value, args: &Value) -> JsonRpcResponse {
1168 let (_, graph) = match self.resolve_graph(args) {
1169 Ok(g) => g,
1170 Err(e) => return JsonRpcResponse::error(id, e),
1171 };
1172
1173 let name = match args.get("name").and_then(|v| v.as_str()) {
1174 Some(n) if !n.trim().is_empty() => n,
1175 _ => {
1176 return JsonRpcResponse::error(
1177 id,
1178 JsonRpcError::invalid_params("Missing or empty 'name' argument"),
1179 );
1180 }
1181 };
1182
1183 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
1184
1185 let engine = GroundingEngine::new(graph);
1186 let suggestions = engine.suggest_similar(name, limit);
1187
1188 JsonRpcResponse::success(
1189 id,
1190 json!({
1191 "content": [{
1192 "type": "text",
1193 "text": serde_json::to_string_pretty(&json!({
1194 "query": name,
1195 "suggestions": suggestions,
1196 })).unwrap_or_default()
1197 }]
1198 }),
1199 )
1200 }
1201
1202 fn resolve_workspace_id(&self, args: &Value) -> Result<String, JsonRpcError> {
1209 let raw = args.get("workspace").and_then(|v| v.as_str()).unwrap_or("");
1210 if raw.is_empty() {
1211 return self
1213 .workspace_manager
1214 .get_active()
1215 .map(|s| s.to_string())
1216 .ok_or_else(|| {
1217 JsonRpcError::invalid_params("No workspace specified and none active")
1218 });
1219 }
1220
1221 if raw.starts_with("ws-") {
1223 self.workspace_manager
1225 .list(raw)
1226 .map(|_| raw.to_string())
1227 .map_err(JsonRpcError::invalid_params)
1228 } else {
1229 self.workspace_manager
1232 .list(raw)
1233 .map(|_| raw.to_string())
1234 .map_err(JsonRpcError::invalid_params)
1235 }
1236 }
1237
1238 fn tool_workspace_create(&mut self, id: Value, args: &Value) -> JsonRpcResponse {
1240 let name = match args.get("name").and_then(|v| v.as_str()) {
1241 Some(n) if !n.trim().is_empty() => n,
1242 _ => {
1243 return JsonRpcResponse::error(
1244 id,
1245 JsonRpcError::invalid_params("Missing or empty 'name' argument"),
1246 );
1247 }
1248 };
1249
1250 let ws_id = self.workspace_manager.create(name);
1251
1252 JsonRpcResponse::success(
1253 id,
1254 json!({
1255 "content": [{
1256 "type": "text",
1257 "text": serde_json::to_string_pretty(&json!({
1258 "workspace_id": ws_id,
1259 "name": name,
1260 "message": "Workspace created"
1261 })).unwrap_or_default()
1262 }]
1263 }),
1264 )
1265 }
1266
1267 fn tool_workspace_add(&mut self, id: Value, args: &Value) -> JsonRpcResponse {
1269 let ws_id = match self.resolve_workspace_id(args) {
1270 Ok(ws) => ws,
1271 Err(e) => return JsonRpcResponse::error(id, e),
1272 };
1273
1274 let graph_name = match args.get("graph").and_then(|v| v.as_str()) {
1275 Some(n) if !n.trim().is_empty() => n.to_string(),
1276 _ => {
1277 return JsonRpcResponse::error(
1278 id,
1279 JsonRpcError::invalid_params("Missing or empty 'graph' argument"),
1280 );
1281 }
1282 };
1283
1284 let graph = match self.graphs.get(&graph_name) {
1286 Some(g) => g.clone(),
1287 None => {
1288 return JsonRpcResponse::error(
1289 id,
1290 JsonRpcError::invalid_params(format!("Graph not found: {}", graph_name)),
1291 );
1292 }
1293 };
1294
1295 let role_str = args
1296 .get("role")
1297 .and_then(|v| v.as_str())
1298 .unwrap_or("source");
1299 let role = match ContextRole::parse_str(role_str) {
1300 Some(r) => r,
1301 None => {
1302 return JsonRpcResponse::error(
1303 id,
1304 JsonRpcError::invalid_params(format!(
1305 "Invalid role '{}'. Expected: source, target, reference, comparison",
1306 role_str
1307 )),
1308 );
1309 }
1310 };
1311
1312 let path = args
1313 .get("path")
1314 .and_then(|v| v.as_str())
1315 .unwrap_or(&graph_name)
1316 .to_string();
1317 let language = args
1318 .get("language")
1319 .and_then(|v| v.as_str())
1320 .map(String::from);
1321
1322 match self
1323 .workspace_manager
1324 .add_context(&ws_id, &path, role, language, graph)
1325 {
1326 Ok(ctx_id) => JsonRpcResponse::success(
1327 id,
1328 json!({
1329 "content": [{
1330 "type": "text",
1331 "text": serde_json::to_string_pretty(&json!({
1332 "context_id": ctx_id,
1333 "workspace_id": ws_id,
1334 "graph": graph_name,
1335 "message": "Context added to workspace"
1336 })).unwrap_or_default()
1337 }]
1338 }),
1339 ),
1340 Err(e) => JsonRpcResponse::error(id, JsonRpcError::invalid_params(e)),
1341 }
1342 }
1343
1344 fn tool_workspace_list(&self, id: Value, args: &Value) -> JsonRpcResponse {
1346 let ws_id = match self.resolve_workspace_id(args) {
1347 Ok(ws) => ws,
1348 Err(e) => return JsonRpcResponse::error(id, e),
1349 };
1350
1351 match self.workspace_manager.list(&ws_id) {
1352 Ok(workspace) => {
1353 let contexts: Vec<Value> = workspace
1354 .contexts
1355 .iter()
1356 .map(|c| {
1357 json!({
1358 "id": c.id,
1359 "role": c.role.label(),
1360 "path": c.path,
1361 "language": c.language,
1362 "unit_count": c.graph.units().len(),
1363 })
1364 })
1365 .collect();
1366
1367 JsonRpcResponse::success(
1368 id,
1369 json!({
1370 "content": [{
1371 "type": "text",
1372 "text": serde_json::to_string_pretty(&json!({
1373 "workspace_id": ws_id,
1374 "name": workspace.name,
1375 "context_count": workspace.contexts.len(),
1376 "contexts": contexts,
1377 })).unwrap_or_default()
1378 }]
1379 }),
1380 )
1381 }
1382 Err(e) => JsonRpcResponse::error(id, JsonRpcError::invalid_params(e)),
1383 }
1384 }
1385
1386 fn tool_workspace_query(&self, id: Value, args: &Value) -> JsonRpcResponse {
1388 let ws_id = match self.resolve_workspace_id(args) {
1389 Ok(ws) => ws,
1390 Err(e) => return JsonRpcResponse::error(id, e),
1391 };
1392
1393 let query = match args.get("query").and_then(|v| v.as_str()) {
1394 Some(q) if !q.trim().is_empty() => q,
1395 _ => {
1396 return JsonRpcResponse::error(
1397 id,
1398 JsonRpcError::invalid_params("Missing or empty 'query' argument"),
1399 );
1400 }
1401 };
1402
1403 let role_filters: Vec<String> = args
1404 .get("roles")
1405 .and_then(|v| v.as_array())
1406 .map(|arr| {
1407 arr.iter()
1408 .filter_map(|v| v.as_str().map(|s| s.to_lowercase()))
1409 .collect()
1410 })
1411 .unwrap_or_default();
1412
1413 match self.workspace_manager.query_all(&ws_id, query) {
1414 Ok(results) => {
1415 let mut filtered = results;
1416 if !role_filters.is_empty() {
1417 filtered.retain(|r| role_filters.contains(&r.context_role.label().to_string()));
1418 }
1419
1420 let output: Vec<Value> = filtered
1421 .iter()
1422 .map(|r| {
1423 json!({
1424 "context_id": r.context_id,
1425 "role": r.context_role.label(),
1426 "matches": r.matches.iter().map(|m| json!({
1427 "unit_id": m.unit_id,
1428 "name": m.name,
1429 "qualified_name": m.qualified_name,
1430 "type": m.unit_type,
1431 "file": m.file_path,
1432 })).collect::<Vec<_>>(),
1433 })
1434 })
1435 .collect();
1436
1437 JsonRpcResponse::success(
1438 id,
1439 json!({
1440 "content": [{
1441 "type": "text",
1442 "text": serde_json::to_string_pretty(&output).unwrap_or_default()
1443 }]
1444 }),
1445 )
1446 }
1447 Err(e) => JsonRpcResponse::error(id, JsonRpcError::invalid_params(e)),
1448 }
1449 }
1450
1451 fn tool_workspace_compare(&self, id: Value, args: &Value) -> JsonRpcResponse {
1453 let ws_id = match self.resolve_workspace_id(args) {
1454 Ok(ws) => ws,
1455 Err(e) => return JsonRpcResponse::error(id, e),
1456 };
1457
1458 let symbol = match args.get("symbol").and_then(|v| v.as_str()) {
1459 Some(s) if !s.trim().is_empty() => s,
1460 _ => {
1461 return JsonRpcResponse::error(
1462 id,
1463 JsonRpcError::invalid_params("Missing or empty 'symbol' argument"),
1464 );
1465 }
1466 };
1467
1468 match self.workspace_manager.compare(&ws_id, symbol) {
1469 Ok(cmp) => {
1470 let contexts: Vec<Value> = cmp
1471 .contexts
1472 .iter()
1473 .map(|c| {
1474 json!({
1475 "context_id": c.context_id,
1476 "role": c.role.label(),
1477 "found": c.found,
1478 "unit_type": c.unit_type,
1479 "signature": c.signature,
1480 "file_path": c.file_path,
1481 })
1482 })
1483 .collect();
1484
1485 JsonRpcResponse::success(
1486 id,
1487 json!({
1488 "content": [{
1489 "type": "text",
1490 "text": serde_json::to_string_pretty(&json!({
1491 "symbol": cmp.symbol,
1492 "semantic_match": cmp.semantic_match,
1493 "structural_diff": cmp.structural_diff,
1494 "contexts": contexts,
1495 })).unwrap_or_default()
1496 }]
1497 }),
1498 )
1499 }
1500 Err(e) => JsonRpcResponse::error(id, JsonRpcError::invalid_params(e)),
1501 }
1502 }
1503
1504 fn tool_workspace_xref(&self, id: Value, args: &Value) -> JsonRpcResponse {
1506 let ws_id = match self.resolve_workspace_id(args) {
1507 Ok(ws) => ws,
1508 Err(e) => return JsonRpcResponse::error(id, e),
1509 };
1510
1511 let symbol = match args.get("symbol").and_then(|v| v.as_str()) {
1512 Some(s) if !s.trim().is_empty() => s,
1513 _ => {
1514 return JsonRpcResponse::error(
1515 id,
1516 JsonRpcError::invalid_params("Missing or empty 'symbol' argument"),
1517 );
1518 }
1519 };
1520
1521 match self.workspace_manager.cross_reference(&ws_id, symbol) {
1522 Ok(xref) => {
1523 let found: Vec<Value> = xref
1524 .found_in
1525 .iter()
1526 .map(|(ctx_id, role)| json!({"context_id": ctx_id, "role": role.label()}))
1527 .collect();
1528 let missing: Vec<Value> = xref
1529 .missing_from
1530 .iter()
1531 .map(|(ctx_id, role)| json!({"context_id": ctx_id, "role": role.label()}))
1532 .collect();
1533
1534 JsonRpcResponse::success(
1535 id,
1536 json!({
1537 "content": [{
1538 "type": "text",
1539 "text": serde_json::to_string_pretty(&json!({
1540 "symbol": xref.symbol,
1541 "found_in": found,
1542 "missing_from": missing,
1543 })).unwrap_or_default()
1544 }]
1545 }),
1546 )
1547 }
1548 Err(e) => JsonRpcResponse::error(id, JsonRpcError::invalid_params(e)),
1549 }
1550 }
1551
1552 fn tool_translation_record(&mut self, id: Value, args: &Value) -> JsonRpcResponse {
1558 let ws_id = match self.resolve_workspace_id(args) {
1559 Ok(ws) => ws,
1560 Err(e) => return JsonRpcResponse::error(id, e),
1561 };
1562
1563 let source_symbol = match args.get("source_symbol").and_then(|v| v.as_str()) {
1564 Some(s) if !s.trim().is_empty() => s,
1565 _ => {
1566 return JsonRpcResponse::error(
1567 id,
1568 JsonRpcError::invalid_params("Missing or empty 'source_symbol' argument"),
1569 );
1570 }
1571 };
1572
1573 let target_symbol = args.get("target_symbol").and_then(|v| v.as_str());
1574
1575 let status_str = args
1576 .get("status")
1577 .and_then(|v| v.as_str())
1578 .unwrap_or("not_started");
1579 let status = match TranslationStatus::parse_str(status_str) {
1580 Some(s) => s,
1581 None => {
1582 return JsonRpcResponse::error(
1583 id,
1584 JsonRpcError::invalid_params(format!(
1585 "Invalid status '{}'. Expected: not_started, in_progress, ported, verified, skipped",
1586 status_str
1587 )),
1588 );
1589 }
1590 };
1591
1592 let notes = args.get("notes").and_then(|v| v.as_str()).map(String::from);
1593
1594 let tmap = self
1597 .translation_maps
1598 .entry(ws_id.clone())
1599 .or_insert_with(|| {
1600 let (src, tgt) = if let Ok(ws) = self.workspace_manager.list(&ws_id) {
1602 let src = ws
1603 .contexts
1604 .iter()
1605 .find(|c| c.role == ContextRole::Source)
1606 .map(|c| c.id.clone())
1607 .unwrap_or_default();
1608 let tgt = ws
1609 .contexts
1610 .iter()
1611 .find(|c| c.role == ContextRole::Target)
1612 .map(|c| c.id.clone())
1613 .unwrap_or_default();
1614 (src, tgt)
1615 } else {
1616 (String::new(), String::new())
1617 };
1618 TranslationMap::new(src, tgt)
1619 });
1620
1621 tmap.record(source_symbol, target_symbol, status, notes);
1622
1623 JsonRpcResponse::success(
1624 id,
1625 json!({
1626 "content": [{
1627 "type": "text",
1628 "text": serde_json::to_string_pretty(&json!({
1629 "source_symbol": source_symbol,
1630 "target_symbol": target_symbol,
1631 "status": status_str,
1632 "message": "Translation mapping recorded"
1633 })).unwrap_or_default()
1634 }]
1635 }),
1636 )
1637 }
1638
1639 fn tool_translation_progress(&self, id: Value, args: &Value) -> JsonRpcResponse {
1641 let ws_id = match self.resolve_workspace_id(args) {
1642 Ok(ws) => ws,
1643 Err(e) => return JsonRpcResponse::error(id, e),
1644 };
1645
1646 let progress = match self.translation_maps.get(&ws_id) {
1647 Some(tmap) => tmap.progress(),
1648 None => {
1649 crate::workspace::TranslationProgress {
1651 total: 0,
1652 not_started: 0,
1653 in_progress: 0,
1654 ported: 0,
1655 verified: 0,
1656 skipped: 0,
1657 percent_complete: 0.0,
1658 }
1659 }
1660 };
1661
1662 JsonRpcResponse::success(
1663 id,
1664 json!({
1665 "content": [{
1666 "type": "text",
1667 "text": serde_json::to_string_pretty(&json!({
1668 "workspace": ws_id,
1669 "total": progress.total,
1670 "not_started": progress.not_started,
1671 "in_progress": progress.in_progress,
1672 "ported": progress.ported,
1673 "verified": progress.verified,
1674 "skipped": progress.skipped,
1675 "percent_complete": progress.percent_complete,
1676 })).unwrap_or_default()
1677 }]
1678 }),
1679 )
1680 }
1681
1682 fn tool_translation_remaining(&self, id: Value, args: &Value) -> JsonRpcResponse {
1684 let ws_id = match self.resolve_workspace_id(args) {
1685 Ok(ws) => ws,
1686 Err(e) => return JsonRpcResponse::error(id, e),
1687 };
1688
1689 let module_filter = args
1690 .get("module")
1691 .and_then(|v| v.as_str())
1692 .map(|s| s.to_lowercase());
1693
1694 let remaining = match self.translation_maps.get(&ws_id) {
1695 Some(tmap) => {
1696 let mut items = tmap.remaining();
1697 if let Some(ref module) = module_filter {
1698 items.retain(|m| m.source_symbol.to_lowercase().contains(module.as_str()));
1699 }
1700 items
1701 .iter()
1702 .map(|m| {
1703 json!({
1704 "source_symbol": m.source_symbol,
1705 "status": m.status.label(),
1706 "notes": m.notes,
1707 })
1708 })
1709 .collect::<Vec<_>>()
1710 }
1711 None => Vec::new(),
1712 };
1713
1714 JsonRpcResponse::success(
1715 id,
1716 json!({
1717 "content": [{
1718 "type": "text",
1719 "text": serde_json::to_string_pretty(&json!({
1720 "workspace": ws_id,
1721 "remaining_count": remaining.len(),
1722 "remaining": remaining,
1723 })).unwrap_or_default()
1724 }]
1725 }),
1726 )
1727 }
1728}
1729
1730fn truncate_json_summary(value: &Value, max_len: usize) -> String {
1732 let s = value.to_string();
1733 if s.len() <= max_len {
1734 s
1735 } else {
1736 format!("{}...", &s[..max_len])
1737 }
1738}
1739
1740impl Default for McpServer {
1741 fn default() -> Self {
1742 Self::new()
1743 }
1744}