Skip to main content

agentic_codebase/mcp/
server.rs

1//! MCP server implementation.
2//!
3//! Synchronous JSON-RPC 2.0 server that exposes code graph operations
4//! through the Model Context Protocol. All operations are in-process
5//! with no async runtime required.
6
7use 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
22/// MCP server capability information.
23const SERVER_NAME: &str = "agentic-codebase";
24/// MCP server version.
25const SERVER_VERSION: &str = "0.1.0";
26/// MCP protocol version supported.
27const PROTOCOL_VERSION: &str = "2024-11-05";
28
29/// Record of a tool call or analysis context entry.
30#[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/// A synchronous MCP server that handles JSON-RPC 2.0 messages.
39///
40/// Holds loaded code graphs and dispatches tool/resource/prompt requests
41/// to the appropriate handler.
42#[derive(Debug)]
43pub struct McpServer {
44    /// Loaded code graphs keyed by name.
45    graphs: HashMap<String, CodeGraph>,
46    /// Query engine for executing queries.
47    engine: QueryEngine,
48    /// Whether the server has been initialised.
49    initialized: bool,
50    /// Log of operations with context for this session.
51    operation_log: Vec<OperationRecord>,
52    /// Timestamp when this session started.
53    session_start_time: Option<u64>,
54    /// Multi-context workspace manager.
55    workspace_manager: WorkspaceManager,
56    /// Translation maps keyed by workspace ID.
57    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    /// Create a new MCP server with no loaded graphs.
81    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    /// Load a code graph into the server under the given name.
94    pub fn load_graph(&mut self, name: String, graph: CodeGraph) {
95        self.graphs.insert(name, graph);
96    }
97
98    /// Remove a loaded code graph.
99    pub fn unload_graph(&mut self, name: &str) -> Option<CodeGraph> {
100        self.graphs.remove(name)
101    }
102
103    /// Get a reference to a loaded graph by name.
104    pub fn get_graph(&self, name: &str) -> Option<&CodeGraph> {
105        self.graphs.get(name)
106    }
107
108    /// List all loaded graph names.
109    pub fn graph_names(&self) -> Vec<&str> {
110        self.graphs.keys().map(|s| s.as_str()).collect()
111    }
112
113    /// Check if the server has been initialised.
114    pub fn is_initialized(&self) -> bool {
115        self.initialized
116    }
117
118    /// Handle a raw JSON-RPC message string.
119    ///
120    /// Parses the message, dispatches to the appropriate handler, and
121    /// returns the serialised JSON-RPC response.
122    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    /// Handle a parsed JSON-RPC request.
140    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    /// Handle JSON-RPC notifications (messages without an `id`).
155    ///
156    /// Notification methods intentionally produce no response frame.
157    fn handle_notification(&mut self, method: &str, _params: &Value) {
158        if method == "notifications/initialized" {
159            self.initialized = true;
160        }
161    }
162
163    // ========================================================================
164    // Method handlers
165    // ========================================================================
166
167    /// Handle the "initialize" method.
168    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    /// Handle the "shutdown" method.
195    fn handle_shutdown(&mut self, id: Value) -> JsonRpcResponse {
196        self.initialized = false;
197        JsonRpcResponse::success(id, json!(null))
198    }
199
200    /// Handle "tools/list".
201    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                    // ── Grounding tools ──────────────────────────────────
289                    {
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                    // ── Workspace tools ──────────────────────────────────
333                    {
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                    // ── Translation tools ────────────────────────────────
408                    {
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    /// Handle "tools/call".
452    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            // Grounding tools
475            "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 tools
479            "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 tools
486            "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        // Auto-log the tool call (skip analysis_log to avoid recursion).
498        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    /// Handle "resources/list".
518    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    /// Handle "resources/read".
540    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        // Parse URI: acb://graphs/{name}/stats or acb://graphs/{name}/units
552        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    /// Handle "prompts/list".
625    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    // ========================================================================
673    // Tool implementations
674    // ========================================================================
675
676    /// Resolve a graph name from arguments, defaulting to the first loaded graph.
677    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            // Use the first graph if available.
685            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    /// Tool: symbol_lookup.
698    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    /// Tool: impact_analysis.
773    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    /// Tool: graph_stats.
865    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    /// Tool: list_units.
889    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    /// Tool: analysis_log — record the intent/context behind a code analysis.
945    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    /// Access the operation log.
998    pub fn operation_log(&self) -> &[OperationRecord] {
999        &self.operation_log
1000    }
1001
1002    /// Access the workspace manager.
1003    pub fn workspace_manager(&self) -> &WorkspaceManager {
1004        &self.workspace_manager
1005    }
1006
1007    /// Access the workspace manager mutably.
1008    pub fn workspace_manager_mut(&mut self) -> &mut WorkspaceManager {
1009        &mut self.workspace_manager
1010    }
1011
1012    // ========================================================================
1013    // Grounding tool implementations
1014    // ========================================================================
1015
1016    /// Tool: codebase_ground — verify a claim about code has graph evidence.
1017    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        // In strict mode, Partial is treated as Ungrounded.
1039        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    /// Tool: codebase_evidence — get graph evidence for a symbol name.
1106    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        // Apply type filters if provided.
1136        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    /// Tool: codebase_suggest — find symbols similar to a name.
1166    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    // ========================================================================
1202    // Workspace tool implementations
1203    // ========================================================================
1204
1205    /// Resolve a workspace ID from arguments. Accepts workspace ID directly
1206    /// or tries to match by name.
1207    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            // Try active workspace.
1211            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 it looks like a workspace ID (starts with "ws-"), use directly.
1219        if raw.starts_with("ws-") {
1220            // Validate it exists.
1221            self.workspace_manager
1222                .list(raw)
1223                .map(|_| raw.to_string())
1224                .map_err(|e| JsonRpcError::invalid_params(e))
1225        } else {
1226            // Try to find by name — iterate all workspaces. We need to expose
1227            // this through the manager. For now, just treat it as an ID.
1228            self.workspace_manager
1229                .list(raw)
1230                .map(|_| raw.to_string())
1231                .map_err(|e| JsonRpcError::invalid_params(e))
1232        }
1233    }
1234
1235    /// Tool: workspace_create.
1236    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    /// Tool: workspace_add — add a loaded graph as a context.
1265    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        // Clone the graph to add to workspace (graphs remain available in the server).
1282        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    /// Tool: workspace_list.
1342    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    /// Tool: workspace_query.
1384    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    /// Tool: workspace_compare.
1449    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    /// Tool: workspace_xref.
1502    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    // ========================================================================
1550    // Translation tool implementations
1551    // ========================================================================
1552
1553    /// Tool: translation_record.
1554    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        // Get or create translation map for this workspace.
1595        // Use workspace's first source and first target context IDs.
1596        let tmap = self
1597            .translation_maps
1598            .entry(ws_id.clone())
1599            .or_insert_with(|| {
1600                // Find source and target context IDs from the workspace.
1601                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    /// Tool: translation_progress.
1640    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                // No translation map yet — return zeros.
1650                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    /// Tool: translation_remaining.
1683    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
1730/// Truncate a JSON value to a short summary string.
1731fn 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}