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