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