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