Skip to main content

codemem_mcp/
lib.rs

1//! codemem-mcp: MCP server for Codemem (JSON-RPC 2.0 over stdio).
2//!
3//! Implements 43 tools: store_memory, recall_memory, update_memory,
4//! delete_memory, associate_memories, graph_traverse, summary_tree,
5//! codemem_stats, codemem_health,
6//! index_codebase, search_symbols, get_symbol_info, get_dependencies, get_impact,
7//! get_clusters, get_cross_repo, get_pagerank, search_code, set_scoring_weights,
8//! export_memories, import_memories, recall_with_expansion, list_namespaces,
9//! namespace_stats, delete_namespace, consolidate_decay, consolidate_creative,
10//! consolidate_cluster, consolidate_forget, consolidation_status,
11//! recall_with_impact, get_decision_chain, detect_patterns, pattern_insights,
12//! refine_memory, split_memory, merge_memories, consolidate_summarize,
13//! codemem_metrics, session_checkpoint,
14//! enrich_git_history, enrich_security, enrich_performance.
15//!
16//! Transport: Newline-delimited JSON-RPC messages over stdio.
17//! All logging goes to stderr; stdout is reserved for JSON-RPC only.
18
19use codemem_core::{
20    CodememError, GraphBackend, MemoryType, ScoringWeights, StorageBackend, VectorBackend,
21};
22use codemem_graph::GraphEngine;
23use codemem_storage::Storage;
24use codemem_vector::HnswIndex;
25use serde_json::{json, Value};
26use std::io::{self, BufRead};
27use std::path::{Path, PathBuf};
28use std::sync::{Mutex, RwLock};
29
30pub mod bm25;
31pub(crate) mod compress;
32#[cfg(feature = "http")]
33pub mod http;
34pub mod metrics;
35pub mod patterns;
36pub mod scoring;
37pub mod tools_consolidation;
38pub mod tools_enrich;
39pub mod tools_graph;
40pub mod tools_memory;
41pub mod tools_recall;
42pub mod types;
43
44#[cfg(test)]
45pub(crate) mod test_helpers;
46
47// Re-export public types for downstream consumers.
48pub use types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, ToolContent, ToolResult};
49
50// Re-export BM25 index type for downstream consumers.
51pub use bm25::Bm25Index;
52
53use scoring::write_response;
54use types::IndexCache;
55
56// ── MCP Server ──────────────────────────────────────────────────────────────
57
58/// MCP server that reads JSON-RPC from stdin, writes responses to stdout.
59/// Holds Storage, HnswIndex, and GraphEngine for real tool dispatch.
60pub struct McpServer {
61    pub name: String,
62    pub version: String,
63    pub(crate) storage: Box<dyn StorageBackend>,
64    pub(crate) vector: Mutex<HnswIndex>,
65    pub(crate) graph: Mutex<GraphEngine>,
66    /// Optional embedding provider (None if not configured).
67    pub(crate) embeddings: Option<Mutex<Box<dyn codemem_embeddings::EmbeddingProvider>>>,
68    /// Path to the database file, used to derive the index save path.
69    pub(crate) db_path: Option<PathBuf>,
70    /// Cached index results for structural queries.
71    pub(crate) index_cache: Mutex<Option<IndexCache>>,
72    /// Configurable scoring weights for the 9-component hybrid scoring system.
73    pub(crate) scoring_weights: RwLock<ScoringWeights>,
74    /// BM25 index for code-aware token overlap scoring.
75    /// Updated incrementally on store/update/delete operations.
76    pub(crate) bm25_index: Mutex<bm25::Bm25Index>,
77    /// Loaded configuration (used by scoring_weights initialization and persistence).
78    #[allow(dead_code)]
79    pub(crate) config: codemem_core::CodememConfig,
80    /// Operational metrics collector.
81    pub(crate) metrics: std::sync::Arc<metrics::InMemoryMetrics>,
82}
83
84impl McpServer {
85    /// Create a server with storage, vector, graph, and optional embeddings backends.
86    pub fn new(
87        storage: Box<dyn StorageBackend>,
88        vector: HnswIndex,
89        graph: GraphEngine,
90        embeddings: Option<Box<dyn codemem_embeddings::EmbeddingProvider>>,
91    ) -> Self {
92        let config = codemem_core::CodememConfig::load_or_default();
93        Self {
94            name: "codemem".to_string(),
95            version: env!("CARGO_PKG_VERSION").to_string(),
96            storage,
97            vector: Mutex::new(vector),
98            graph: Mutex::new(graph),
99            embeddings: embeddings.map(Mutex::new),
100            db_path: None,
101            index_cache: Mutex::new(None),
102            scoring_weights: RwLock::new(config.scoring.clone()),
103            bm25_index: Mutex::new(bm25::Bm25Index::new()),
104            config,
105            metrics: std::sync::Arc::new(metrics::InMemoryMetrics::new()),
106        }
107    }
108
109    /// Create a server from a database path, loading all backends.
110    pub fn from_db_path(db_path: &Path) -> Result<Self, CodememError> {
111        let storage = Storage::open(db_path)?;
112        let mut vector = HnswIndex::with_defaults()?;
113
114        // Load existing vector index if it exists
115        let index_path = db_path.with_extension("idx");
116        if index_path.exists() {
117            vector.load(&index_path)?;
118        }
119
120        // Load graph from storage
121        let graph = GraphEngine::from_storage(&storage)?;
122
123        // Try loading embeddings (optional - selects provider from env vars)
124        let embeddings = codemem_embeddings::from_env().ok();
125
126        let mut server = Self::new(Box::new(storage), vector, graph, embeddings);
127        server.db_path = Some(db_path.to_path_buf());
128
129        // Recompute centrality metrics (PageRank + betweenness) on startup
130        server.lock_graph()?.recompute_centrality();
131
132        // Populate BM25 index from all existing memories
133        if let Ok(ids) = server.storage.list_memory_ids() {
134            let mut bm25 = server.lock_bm25()?;
135            for id in &ids {
136                if let Ok(Some(memory)) = server.storage.get_memory(id) {
137                    bm25.add_document(id, &memory.content);
138                }
139            }
140        }
141
142        Ok(server)
143    }
144
145    /// Create a minimal server for testing (no backends wired).
146    pub fn for_testing() -> Self {
147        let storage = Storage::open_in_memory().unwrap();
148        let vector = HnswIndex::with_defaults().unwrap();
149        let graph = GraphEngine::new();
150        Self::new(Box::new(storage), vector, graph, None)
151    }
152
153    // ── Lock Helpers ─────────────────────────────────────────────────────────
154
155    pub(crate) fn lock_vector(&self) -> Result<std::sync::MutexGuard<'_, HnswIndex>, CodememError> {
156        self.vector
157            .lock()
158            .map_err(|e| CodememError::LockPoisoned(format!("vector: {e}")))
159    }
160
161    pub fn lock_graph(&self) -> Result<std::sync::MutexGuard<'_, GraphEngine>, CodememError> {
162        self.graph
163            .lock()
164            .map_err(|e| CodememError::LockPoisoned(format!("graph: {e}")))
165    }
166
167    pub(crate) fn lock_bm25(
168        &self,
169    ) -> Result<std::sync::MutexGuard<'_, bm25::Bm25Index>, CodememError> {
170        self.bm25_index
171            .lock()
172            .map_err(|e| CodememError::LockPoisoned(format!("bm25: {e}")))
173    }
174
175    pub(crate) fn lock_embeddings(
176        &self,
177    ) -> Result<
178        Option<std::sync::MutexGuard<'_, Box<dyn codemem_embeddings::EmbeddingProvider>>>,
179        CodememError,
180    > {
181        match &self.embeddings {
182            Some(m) => Ok(Some(m.lock().map_err(|e| {
183                CodememError::LockPoisoned(format!("embeddings: {e}"))
184            })?)),
185            None => Ok(None),
186        }
187    }
188
189    pub(crate) fn lock_index_cache(
190        &self,
191    ) -> Result<std::sync::MutexGuard<'_, Option<types::IndexCache>>, CodememError> {
192        self.index_cache
193            .lock()
194            .map_err(|e| CodememError::LockPoisoned(format!("index_cache: {e}")))
195    }
196
197    pub(crate) fn scoring_weights(
198        &self,
199    ) -> Result<std::sync::RwLockReadGuard<'_, codemem_core::ScoringWeights>, CodememError> {
200        self.scoring_weights
201            .read()
202            .map_err(|e| CodememError::LockPoisoned(format!("scoring_weights read: {e}")))
203    }
204
205    pub(crate) fn scoring_weights_mut(
206        &self,
207    ) -> Result<std::sync::RwLockWriteGuard<'_, codemem_core::ScoringWeights>, CodememError> {
208        self.scoring_weights
209            .write()
210            .map_err(|e| CodememError::LockPoisoned(format!("scoring_weights write: {e}")))
211    }
212
213    // ── Contextual Enrichment ────────────────────────────────────────────────
214    // Prepend metadata context to text before embedding, following the
215    // "contextual embeddings" methodology: enriched vectors capture semantic
216    // relationships that raw content alone would miss.
217
218    /// Build contextual text for a memory node.
219    /// Prepends memory type, tags, namespace, and graph relationships so the
220    /// embedding captures the memory's role, not just its content.
221    pub(crate) fn enrich_memory_text(
222        &self,
223        content: &str,
224        memory_type: MemoryType,
225        tags: &[String],
226        namespace: Option<&str>,
227        node_id: Option<&str>,
228    ) -> String {
229        let mut ctx = String::new();
230
231        // Memory type
232        ctx.push_str(&format!("[{}]", memory_type));
233
234        // Namespace
235        if let Some(ns) = namespace {
236            ctx.push_str(&format!(" [namespace:{}]", ns));
237        }
238
239        // Tags
240        if !tags.is_empty() {
241            ctx.push_str(&format!(" [tags:{}]", tags.join(",")));
242        }
243
244        // Graph relationships — pull connected edges for this node
245        if let Some(nid) = node_id {
246            let graph = match self.lock_graph() {
247                Ok(g) => g,
248                Err(_) => return format!("{ctx}\n{content}"),
249            };
250            if let Ok(edges) = graph.get_edges(nid) {
251                let mut rels: Vec<String> = Vec::new();
252                for edge in edges.iter().take(8) {
253                    let other = if edge.src == nid {
254                        &edge.dst
255                    } else {
256                        &edge.src
257                    };
258                    // Resolve the other node's label for readable context
259                    let label = graph
260                        .get_node(other)
261                        .ok()
262                        .flatten()
263                        .map(|n| n.label.clone())
264                        .unwrap_or_else(|| other.to_string());
265                    let dir = if edge.src == nid { "->" } else { "<-" };
266                    rels.push(format!("{dir} {} ({})", label, edge.relationship));
267                }
268                if !rels.is_empty() {
269                    ctx.push_str(&format!("\nRelated: {}", rels.join("; ")));
270                }
271            }
272        }
273
274        format!("{ctx}\n{content}")
275    }
276
277    /// Build contextual text for a code symbol.
278    /// Prepends symbol kind, file path, parent, visibility, and resolved
279    /// edges so the embedding captures the symbol's structural context.
280    pub(crate) fn enrich_symbol_text(
281        &self,
282        sym: &codemem_index::Symbol,
283        edges: &[codemem_index::ResolvedEdge],
284    ) -> String {
285        let mut ctx = String::new();
286
287        // Symbol kind + visibility
288        ctx.push_str(&format!("[{} {}]", sym.visibility, sym.kind));
289
290        // File path
291        ctx.push_str(&format!(" File: {}", sym.file_path));
292
293        // Parent (e.g., struct for a method)
294        if let Some(ref parent) = sym.parent {
295            ctx.push_str(&format!(" Parent: {}", parent));
296        }
297
298        // Resolved edges — calls, imports, inherits, etc.
299        let related: Vec<String> = edges
300            .iter()
301            .filter(|e| {
302                e.source_qualified_name == sym.qualified_name
303                    || e.target_qualified_name == sym.qualified_name
304            })
305            .take(8)
306            .map(|e| {
307                if e.source_qualified_name == sym.qualified_name {
308                    format!("-> {} ({})", e.target_qualified_name, e.relationship)
309                } else {
310                    format!("<- {} ({})", e.source_qualified_name, e.relationship)
311                }
312            })
313            .collect();
314        if !related.is_empty() {
315            ctx.push_str(&format!("\nRelated: {}", related.join("; ")));
316        }
317
318        // Signature + doc comment
319        let mut body = format!("{}: {}", sym.qualified_name, sym.signature);
320        if let Some(ref doc) = sym.doc_comment {
321            body.push('\n');
322            body.push_str(doc);
323        }
324
325        format!("{ctx}\n{body}")
326    }
327
328    /// Build contextual text for a code chunk before embedding.
329    /// Includes file path, parent symbol, node kind, and the chunk text
330    /// (truncated to 4000 chars).
331    pub(crate) fn enrich_chunk_text(&self, chunk: &codemem_index::CodeChunk) -> String {
332        let mut ctx = String::new();
333        ctx.push_str(&format!("[chunk:{}]", chunk.node_kind));
334        ctx.push_str(&format!(" File: {}", chunk.file_path));
335        ctx.push_str(&format!(" Lines: {}-{}", chunk.line_start, chunk.line_end));
336        if let Some(ref parent) = chunk.parent_symbol {
337            ctx.push_str(&format!(" Parent: {}", parent));
338        }
339
340        let body = if chunk.text.len() > 4000 {
341            &chunk.text[..4000]
342        } else {
343            &chunk.text
344        };
345
346        format!("{ctx}\n{body}")
347    }
348
349    // ── Auto-linking ─────────────────────────────────────────────────────
350
351    /// Scan memory content for file paths and qualified symbol names that exist
352    /// as graph nodes, and create RELATES_TO edges with weight 0.5.
353    /// Skips any IDs that already appear in `existing_links`.
354    /// Returns the count of new edges created.
355    pub(crate) fn auto_link_to_code_nodes(
356        &self,
357        memory_id: &str,
358        content: &str,
359        existing_links: &[String],
360    ) -> usize {
361        let mut graph = match self.lock_graph() {
362            Ok(g) => g,
363            Err(_) => return 0,
364        };
365
366        let existing_set: std::collections::HashSet<&str> =
367            existing_links.iter().map(|s| s.as_str()).collect();
368
369        // Extract candidate node IDs from content
370        let mut candidates: Vec<String> = Vec::new();
371
372        // Look for file paths: word tokens containing '/' or ending in common extensions
373        for word in content.split_whitespace() {
374            let cleaned = word.trim_matches(|c: char| {
375                !c.is_alphanumeric() && c != '/' && c != '.' && c != '_' && c != '-' && c != ':'
376            });
377            if cleaned.is_empty() {
378                continue;
379            }
380            // file: prefix pattern
381            if cleaned.contains('/') || cleaned.contains('.') {
382                let file_id = format!("file:{cleaned}");
383                if !existing_set.contains(file_id.as_str()) {
384                    candidates.push(file_id);
385                }
386            }
387            // Qualified symbol names (contains ::)
388            if cleaned.contains("::") {
389                let sym_id = format!("sym:{cleaned}");
390                if !existing_set.contains(sym_id.as_str()) {
391                    candidates.push(sym_id);
392                }
393            }
394        }
395
396        let now = chrono::Utc::now();
397        let mut created = 0;
398        let mut seen = std::collections::HashSet::new();
399
400        for candidate_id in &candidates {
401            if !seen.insert(candidate_id.clone()) {
402                continue;
403            }
404            // Only create edge if the target node exists in the graph
405            if graph.get_node(candidate_id).ok().flatten().is_none() {
406                continue;
407            }
408            let edge = codemem_core::Edge {
409                id: format!("{memory_id}-RELATES_TO-{candidate_id}"),
410                src: memory_id.to_string(),
411                dst: candidate_id.clone(),
412                relationship: codemem_core::RelationshipType::RelatesTo,
413                weight: 0.5,
414                properties: std::collections::HashMap::from([(
415                    "auto_linked".to_string(),
416                    serde_json::json!(true),
417                )]),
418                created_at: now,
419                valid_from: None,
420                valid_to: None,
421            };
422            if self.storage.insert_graph_edge(&edge).is_ok() && graph.add_edge(edge).is_ok() {
423                created += 1;
424            }
425        }
426
427        created
428    }
429
430    // ── Public Accessors (for REST API layer) ─────────────────────────────
431
432    /// Access the underlying storage backend.
433    pub fn storage(&self) -> &dyn StorageBackend {
434        &*self.storage
435    }
436
437    /// Access the graph engine (mutex-protected).
438    pub fn graph(&self) -> &Mutex<GraphEngine> {
439        &self.graph
440    }
441
442    /// Access the vector index (mutex-protected).
443    pub fn vector(&self) -> &Mutex<HnswIndex> {
444        &self.vector
445    }
446
447    /// Access the embedding provider (mutex-protected, optional).
448    pub fn embeddings(&self) -> Option<&Mutex<Box<dyn codemem_embeddings::EmbeddingProvider>>> {
449        self.embeddings.as_ref()
450    }
451
452    /// Access the BM25 index (mutex-protected).
453    pub fn bm25(&self) -> &Mutex<bm25::Bm25Index> {
454        &self.bm25_index
455    }
456
457    /// Reload the in-memory graph from the database.
458    ///
459    /// This is needed when the graph was modified by a separate process
460    /// (e.g., MCP stdio indexing while the API server is running).
461    pub fn reload_graph(&self) -> Result<(), CodememError> {
462        let new_graph = GraphEngine::from_storage(&*self.storage)?;
463        let mut graph = self.lock_graph()?;
464        *graph = new_graph;
465        graph.recompute_centrality();
466        Ok(())
467    }
468
469    /// Access the database path.
470    pub fn db_path(&self) -> Option<&Path> {
471        self.db_path.as_deref()
472    }
473
474    /// Access the loaded configuration.
475    pub fn config(&self) -> &codemem_core::CodememConfig {
476        &self.config
477    }
478
479    /// Access the operational metrics collector.
480    pub fn metrics_collector(&self) -> &std::sync::Arc<metrics::InMemoryMetrics> {
481        &self.metrics
482    }
483
484    /// Save the HNSW index to disk. The index file path is derived from
485    /// the database path with an `.idx` extension. No-op if db_path is None
486    /// (e.g., in-memory / testing mode).
487    pub fn save_index(&self) {
488        if let Some(ref db_path) = self.db_path {
489            let index_path = db_path.with_extension("idx");
490            match self.lock_vector() {
491                Ok(vec) => {
492                    if let Err(e) = vec.save(&index_path) {
493                        tracing::warn!("Failed to save vector index: {e}");
494                    }
495                }
496                Err(e) => {
497                    tracing::warn!("Failed to acquire vector lock for save: {e}");
498                }
499            }
500        }
501    }
502
503    /// Run the MCP server over stdio. Convenience method that creates a
504    /// `StdioTransport` and runs it. Blocks until stdin is closed.
505    pub fn run(&self) -> io::Result<()> {
506        let transport = StdioTransport::new(self);
507        transport.run()
508    }
509
510    pub fn handle_notification(&self, method: &str) {
511        match method {
512            "notifications/initialized" => {
513                tracing::info!("Client initialized, codemem MCP server ready");
514            }
515            "notifications/cancelled" => {
516                tracing::debug!("Request cancelled by client");
517            }
518            _ => {
519                tracing::debug!("Unknown notification: {method}");
520            }
521        }
522    }
523
524    pub fn handle_request(
525        &self,
526        method: &str,
527        params: Option<&Value>,
528        id: Value,
529    ) -> JsonRpcResponse {
530        match method {
531            "initialize" => self.handle_initialize(id),
532            "tools/list" => self.handle_tools_list(id),
533            "tools/call" => self.handle_tools_call(id, params),
534            "ping" => JsonRpcResponse::success(id, json!({})),
535            _ => JsonRpcResponse::error(id, -32601, format!("Method not found: {method}")),
536        }
537    }
538
539    fn handle_initialize(&self, id: Value) -> JsonRpcResponse {
540        JsonRpcResponse::success(
541            id,
542            json!({
543                "protocolVersion": "2024-11-05",
544                "capabilities": {
545                    "tools": { "listChanged": false }
546                },
547                "serverInfo": {
548                    "name": self.name,
549                    "version": self.version
550                }
551            }),
552        )
553    }
554
555    fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
556        JsonRpcResponse::success(
557            id,
558            json!({
559                "tools": tool_definitions()
560            }),
561        )
562    }
563
564    fn handle_tools_call(&self, id: Value, params: Option<&Value>) -> JsonRpcResponse {
565        let params = match params {
566            Some(p) => p,
567            None => return JsonRpcResponse::error(id, -32602, "Missing params"),
568        };
569
570        let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
571        let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
572
573        let result = self.dispatch_tool(tool_name, &arguments);
574
575        match serde_json::to_value(result) {
576            Ok(v) => JsonRpcResponse::success(id, v),
577            Err(e) => JsonRpcResponse::error(id, -32603, format!("Serialization error: {e}")),
578        }
579    }
580
581    // ── Tool Dispatch ───────────────────────────────────────────────────────
582
583    fn dispatch_tool(&self, name: &str, args: &Value) -> ToolResult {
584        let start = std::time::Instant::now();
585        let result = self.dispatch_tool_inner(name, args);
586        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
587        codemem_core::Metrics::record_latency(&*self.metrics, name, elapsed);
588        codemem_core::Metrics::increment_counter(&*self.metrics, "tool_calls_total", 1);
589        result
590    }
591
592    fn dispatch_tool_inner(&self, name: &str, args: &Value) -> ToolResult {
593        match name {
594            "store_memory" => self.tool_store_memory(args),
595            "recall_memory" => self.tool_recall_memory(args),
596            "update_memory" => self.tool_update_memory(args),
597            "delete_memory" => self.tool_delete_memory(args),
598            "associate_memories" => self.tool_associate_memories(args),
599            "graph_traverse" => self.tool_graph_traverse(args),
600            "summary_tree" => self.tool_summary_tree(args),
601            "codemem_stats" => self.tool_stats(),
602            "codemem_health" => self.tool_health(),
603            "index_codebase" => self.tool_index_codebase(args),
604            "search_symbols" => self.tool_search_symbols(args),
605            "get_symbol_info" => self.tool_get_symbol_info(args),
606            "get_dependencies" => self.tool_get_dependencies(args),
607            "get_impact" => self.tool_get_impact(args),
608            "get_clusters" => self.tool_get_clusters(args),
609            "get_cross_repo" => self.tool_get_cross_repo(args),
610            "get_pagerank" => self.tool_get_pagerank(args),
611            "search_code" => self.tool_search_code(args),
612            "set_scoring_weights" => self.tool_set_scoring_weights(args),
613            "consolidate_decay" => self.tool_consolidate_decay(args),
614            "consolidate_creative" => self.tool_consolidate_creative(args),
615            "consolidate_cluster" => self.tool_consolidate_cluster(args),
616            "consolidate_forget" => self.tool_consolidate_forget(args),
617            "consolidation_status" => self.tool_consolidation_status(),
618            "recall_with_expansion" => self.tool_recall_with_expansion(args),
619            "recall_with_impact" => self.tool_recall_with_impact(args),
620            "get_decision_chain" => self.tool_get_decision_chain(args),
621            "list_namespaces" => self.tool_list_namespaces(),
622            "namespace_stats" => self.tool_namespace_stats(args),
623            "delete_namespace" => self.tool_delete_namespace(args),
624            "export_memories" => self.tool_export_memories(args),
625            "import_memories" => self.tool_import_memories(args),
626            "detect_patterns" => self.tool_detect_patterns(args),
627            "pattern_insights" => self.tool_pattern_insights(args),
628            "refine_memory" => self.tool_refine_memory(args),
629            "split_memory" => self.tool_split_memory(args),
630            "merge_memories" => self.tool_merge_memories(args),
631            "consolidate_summarize" => self.tool_consolidate_summarize(args),
632            "codemem_metrics" => self.tool_metrics(),
633            "enrich_git_history" => self.tool_enrich_git_history(args),
634            "enrich_security" => self.tool_enrich_security(args),
635            "enrich_performance" => self.tool_enrich_performance(args),
636            "session_checkpoint" => self.tool_session_checkpoint(args),
637            _ => ToolResult::tool_error(format!("Unknown tool: {name}")),
638        }
639    }
640}
641
642// ── Stdio Transport ────────────────────────────────────────────────────────
643
644/// Stdio transport for the MCP server.
645/// Reads newline-delimited JSON-RPC from stdin, writes responses to stdout.
646pub struct StdioTransport<'a> {
647    server: &'a McpServer,
648}
649
650impl<'a> StdioTransport<'a> {
651    /// Create a new stdio transport wrapping the given server.
652    pub fn new(server: &'a McpServer) -> Self {
653        Self { server }
654    }
655
656    /// Run the transport loop. Blocks until stdin is closed.
657    pub fn run(&self) -> io::Result<()> {
658        let stdin = io::stdin();
659        let stdout = io::stdout();
660        let mut stdout = stdout.lock();
661
662        for line in stdin.lock().lines() {
663            let line = line?;
664            if line.trim().is_empty() {
665                continue;
666            }
667
668            let request: JsonRpcRequest = match serde_json::from_str(&line) {
669                Ok(req) => req,
670                Err(e) => {
671                    let resp =
672                        JsonRpcResponse::error(Value::Null, -32700, format!("Parse error: {e}"));
673                    write_response(&mut stdout, &resp)?;
674                    continue;
675                }
676            };
677
678            // Notifications (no id) don't get a response
679            if request.id.is_none() {
680                self.server.handle_notification(&request.method);
681                continue;
682            }
683
684            let id = request.id.unwrap();
685            let response = self
686                .server
687                .handle_request(&request.method, request.params.as_ref(), id);
688            write_response(&mut stdout, &response)?;
689        }
690
691        Ok(())
692    }
693}
694
695// ── Tool Definitions ────────────────────────────────────────────────────────
696
697fn tool_definitions() -> Vec<Value> {
698    vec![
699        json!({
700            "name": "store_memory",
701            "description": "Store a new memory with auto-embedding, type classification, and graph linking",
702            "inputSchema": {
703                "type": "object",
704                "properties": {
705                    "content": { "type": "string", "description": "The memory content to store" },
706                    "memory_type": {
707                        "type": "string",
708                        "enum": ["decision", "pattern", "preference", "style", "habit", "insight", "context"],
709                        "description": "Type of memory (default: context)"
710                    },
711                    "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.5 },
712                    "tags": { "type": "array", "items": { "type": "string" } },
713                    "namespace": { "type": "string", "description": "Namespace to scope the memory (e.g. project path)" },
714                    "links": {
715                        "type": "array",
716                        "items": { "type": "string" },
717                        "description": "List of graph node IDs to link this memory to (e.g., structural symbol IDs)"
718                    }
719                },
720                "required": ["content"]
721            }
722        }),
723        json!({
724            "name": "recall_memory",
725            "description": "Semantic search using 9-component hybrid scoring with graph expansion and bridge discovery",
726            "inputSchema": {
727                "type": "object",
728                "properties": {
729                    "query": { "type": "string", "description": "Natural language search query" },
730                    "k": { "type": "integer", "default": 10, "description": "Number of results" },
731                    "memory_type": { "type": "string", "description": "Filter by memory type" },
732                    "namespace": { "type": "string", "description": "Filter results to a specific namespace" },
733                    "exclude_tags": { "type": "array", "items": { "type": "string" }, "description": "Exclude memories with any of these tags (e.g. [\"static-analysis\"])" },
734                    "min_importance": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "Only return memories with importance >= this value" },
735                    "min_confidence": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "Only return memories with confidence >= this value" }
736                },
737                "required": ["query"]
738            }
739        }),
740        json!({
741            "name": "update_memory",
742            "description": "Update an existing memory's content and re-embed",
743            "inputSchema": {
744                "type": "object",
745                "properties": {
746                    "id": { "type": "string" },
747                    "content": { "type": "string" },
748                    "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0 }
749                },
750                "required": ["id", "content"]
751            }
752        }),
753        json!({
754            "name": "delete_memory",
755            "description": "Delete a memory by ID, removing from vector index, graph, and storage",
756            "inputSchema": {
757                "type": "object",
758                "properties": {
759                    "id": { "type": "string" }
760                },
761                "required": ["id"]
762            }
763        }),
764        json!({
765            "name": "associate_memories",
766            "description": "Create a typed relationship between two memories in the knowledge graph",
767            "inputSchema": {
768                "type": "object",
769                "properties": {
770                    "source_id": { "type": "string" },
771                    "target_id": { "type": "string" },
772                    "relationship": {
773                        "type": "string",
774                        "enum": ["RELATES_TO","LEADS_TO","PART_OF","REINFORCES","CONTRADICTS",
775                                 "EVOLVED_INTO","DERIVED_FROM","INVALIDATED_BY","DEPENDS_ON",
776                                 "IMPORTS","EXTENDS","CALLS","CONTAINS","SUPERSEDES","BLOCKS",
777                                 "IMPLEMENTS","INHERITS","SIMILAR_TO","PRECEDED_BY",
778                                 "EXEMPLIFIES","EXPLAINS","SHARES_THEME","SUMMARIZES","CO_CHANGED"]
779                    },
780                    "weight": { "type": "number", "default": 1.0 }
781                },
782                "required": ["source_id", "target_id", "relationship"]
783            }
784        }),
785        json!({
786            "name": "graph_traverse",
787            "description": "Multi-hop graph traversal from a start node with optional filtering by node kind and relationship type",
788            "inputSchema": {
789                "type": "object",
790                "properties": {
791                    "start_id": { "type": "string" },
792                    "max_depth": { "type": "integer", "default": 2 },
793                    "algorithm": { "type": "string", "enum": ["bfs", "dfs"], "default": "bfs" },
794                    "exclude_kinds": {
795                        "type": "array",
796                        "items": { "type": "string", "enum": ["file","package","function","class","module","memory","method","interface","type","constant","endpoint","test","chunk"] },
797                        "description": "Node kinds to exclude from results and traversal (e.g. [\"chunk\"] to skip chunks)"
798                    },
799                    "include_relationships": {
800                        "type": "array",
801                        "items": { "type": "string" },
802                        "description": "Only follow edges of these relationship types (e.g. [\"CALLS\",\"IMPORTS\"]). If omitted, all relationships are followed."
803                    }
804                },
805                "required": ["start_id"]
806            }
807        }),
808        json!({
809            "name": "summary_tree",
810            "description": "Return a hierarchical summary tree (packages → files → symbols). Start from a pkg: node to see the directory structure.",
811            "inputSchema": {
812                "type": "object",
813                "properties": {
814                    "start_id": { "type": "string", "description": "Node ID to start from (e.g. 'pkg:src/')" },
815                    "max_depth": { "type": "integer", "default": 3, "description": "Maximum tree depth" },
816                    "include_chunks": { "type": "boolean", "default": false, "description": "Include chunk nodes in the tree" }
817                },
818                "required": ["start_id"]
819            }
820        }),
821        json!({
822            "name": "codemem_stats",
823            "description": "Get database and index statistics",
824            "inputSchema": { "type": "object", "properties": {} }
825        }),
826        json!({
827            "name": "codemem_health",
828            "description": "Health check across all Codemem subsystems (storage, vector, graph, embeddings)",
829            "inputSchema": { "type": "object", "properties": {} }
830        }),
831        // ── Structural Index Tools ──────────────────────────────────────────
832        json!({
833            "name": "index_codebase",
834            "description": "Index a codebase directory to extract symbols and references using tree-sitter, populating the structural knowledge graph",
835            "inputSchema": {
836                "type": "object",
837                "properties": {
838                    "path": { "type": "string", "description": "Absolute path to the codebase directory to index" }
839                },
840                "required": ["path"]
841            }
842        }),
843        json!({
844            "name": "search_symbols",
845            "description": "Search indexed code symbols by name substring, optionally filtering by kind (function, method, struct, etc.)",
846            "inputSchema": {
847                "type": "object",
848                "properties": {
849                    "query": { "type": "string", "description": "Substring to search for in symbol names" },
850                    "kind": {
851                        "type": "string",
852                        "enum": ["function", "method", "class", "struct", "enum", "interface", "type", "constant", "module", "test"],
853                        "description": "Filter by symbol kind"
854                    },
855                    "limit": { "type": "integer", "default": 20, "description": "Maximum number of results" }
856                },
857                "required": ["query"]
858            }
859        }),
860        json!({
861            "name": "get_symbol_info",
862            "description": "Get full details of a symbol by qualified name, including signature, file path, doc comment, and parent",
863            "inputSchema": {
864                "type": "object",
865                "properties": {
866                    "qualified_name": { "type": "string", "description": "Fully qualified name of the symbol (e.g. 'module::Struct::method')" }
867                },
868                "required": ["qualified_name"]
869            }
870        }),
871        json!({
872            "name": "get_dependencies",
873            "description": "Get graph edges (calls, imports, extends, etc.) connected to a symbol",
874            "inputSchema": {
875                "type": "object",
876                "properties": {
877                    "qualified_name": { "type": "string", "description": "Fully qualified name of the symbol" },
878                    "direction": {
879                        "type": "string",
880                        "enum": ["incoming", "outgoing", "both"],
881                        "default": "both",
882                        "description": "Direction of dependencies to return"
883                    }
884                },
885                "required": ["qualified_name"]
886            }
887        }),
888        json!({
889            "name": "get_impact",
890            "description": "Impact analysis: find all graph nodes reachable from a symbol within N hops (what breaks if this changes?)",
891            "inputSchema": {
892                "type": "object",
893                "properties": {
894                    "qualified_name": { "type": "string", "description": "Fully qualified name of the symbol to analyze" },
895                    "depth": { "type": "integer", "default": 2, "description": "Maximum BFS depth for reachability" }
896                },
897                "required": ["qualified_name"]
898            }
899        }),
900        json!({
901            "name": "get_clusters",
902            "description": "Run Louvain community detection on the knowledge graph to find clusters of related symbols",
903            "inputSchema": {
904                "type": "object",
905                "properties": {
906                    "resolution": { "type": "number", "default": 1.0, "description": "Louvain resolution parameter (higher = more clusters)" }
907                }
908            }
909        }),
910        json!({
911            "name": "get_cross_repo",
912            "description": "Scan for workspace manifests (Cargo.toml, package.json) and report workspace structure and cross-package dependencies",
913            "inputSchema": {
914                "type": "object",
915                "properties": {
916                    "path": { "type": "string", "description": "Path to scan (defaults to the last indexed codebase root)" }
917                }
918            }
919        }),
920        json!({
921            "name": "get_pagerank",
922            "description": "Run PageRank on the full knowledge graph to find the most important/central nodes",
923            "inputSchema": {
924                "type": "object",
925                "properties": {
926                    "top_k": { "type": "integer", "default": 20, "description": "Number of top-ranked nodes to return" },
927                    "damping": { "type": "number", "default": 0.85, "description": "PageRank damping factor" }
928                }
929            }
930        }),
931        json!({
932            "name": "search_code",
933            "description": "Semantic search over indexed code symbols using signature embeddings. Finds functions, types, and methods by meaning rather than exact name match.",
934            "inputSchema": {
935                "type": "object",
936                "properties": {
937                    "query": { "type": "string", "description": "Natural language description of the code you're looking for (e.g. 'parse JSON config', 'HTTP request handler')" },
938                    "k": { "type": "integer", "default": 10, "description": "Number of results to return" }
939                },
940                "required": ["query"]
941            }
942        }),
943        json!({
944            "name": "set_scoring_weights",
945            "description": "Update the 9-component hybrid scoring weights at runtime. Weights are normalized to sum to 1.0. Omitted weights use their default values.",
946            "inputSchema": {
947                "type": "object",
948                "properties": {
949                    "vector_similarity": { "type": "number", "minimum": 0.0, "description": "Weight for vector cosine similarity (default: 0.25)" },
950                    "graph_strength": { "type": "number", "minimum": 0.0, "description": "Weight for graph relationship strength (default: 0.25)" },
951                    "token_overlap": { "type": "number", "minimum": 0.0, "description": "Weight for content token overlap (default: 0.15)" },
952                    "temporal": { "type": "number", "minimum": 0.0, "description": "Weight for temporal alignment (default: 0.10)" },
953                    "tag_matching": { "type": "number", "minimum": 0.0, "description": "Weight for tag matching (default: 0.10)" },
954                    "importance": { "type": "number", "minimum": 0.0, "description": "Weight for importance score (default: 0.05)" },
955                    "confidence": { "type": "number", "minimum": 0.0, "description": "Weight for memory confidence (default: 0.05)" },
956                    "recency": { "type": "number", "minimum": 0.0, "description": "Weight for recency boost (default: 0.05)" }
957                }
958            }
959        }),
960        // ── Export/Import Tools ──────────────────────────────────────────────
961        json!({
962            "name": "export_memories",
963            "description": "Export memories as a JSON array with optional namespace and memory_type filters. Returns memory objects with their graph edges.",
964            "inputSchema": {
965                "type": "object",
966                "properties": {
967                    "namespace": { "type": "string", "description": "Filter by namespace" },
968                    "memory_type": {
969                        "type": "string",
970                        "enum": ["decision", "pattern", "preference", "style", "habit", "insight", "context"],
971                        "description": "Filter by memory type"
972                    },
973                    "limit": { "type": "integer", "default": 100, "description": "Maximum number of memories to export" }
974                }
975            }
976        }),
977        json!({
978            "name": "import_memories",
979            "description": "Import memories from a JSON array. Each object must have at least a 'content' field. Auto-deduplicates by content hash.",
980            "inputSchema": {
981                "type": "object",
982                "properties": {
983                    "memories": {
984                        "type": "array",
985                        "items": {
986                            "type": "object",
987                            "properties": {
988                                "content": { "type": "string", "description": "The memory content (required)" },
989                                "memory_type": {
990                                    "type": "string",
991                                    "enum": ["decision", "pattern", "preference", "style", "habit", "insight", "context"],
992                                    "description": "Type of memory (default: context)"
993                                },
994                                "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "Importance score (default: 0.5)" },
995                                "confidence": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "Confidence score (default: 1.0)" },
996                                "tags": { "type": "array", "items": { "type": "string" } },
997                                "namespace": { "type": "string", "description": "Namespace to scope the memory" },
998                                "metadata": { "type": "object", "description": "Arbitrary metadata key-value pairs" }
999                            },
1000                            "required": ["content"]
1001                        },
1002                        "description": "Array of memory objects to import"
1003                    }
1004                },
1005                "required": ["memories"]
1006            }
1007        }),
1008        // ── Graph-Expanded Recall & Namespace Management ────────────────────
1009        json!({
1010            "name": "recall_with_expansion",
1011            "description": "Semantic search with graph expansion: finds memories via vector similarity then expands through the knowledge graph to discover related memories up to N hops away",
1012            "inputSchema": {
1013                "type": "object",
1014                "properties": {
1015                    "query": { "type": "string", "description": "Natural language search query" },
1016                    "k": { "type": "integer", "default": 5, "description": "Number of results to return" },
1017                    "expansion_depth": { "type": "integer", "default": 1, "description": "Maximum graph hops for expansion (0 = no expansion)" },
1018                    "namespace": { "type": "string", "description": "Filter results to a specific namespace" }
1019                },
1020                "required": ["query"]
1021            }
1022        }),
1023        json!({
1024            "name": "list_namespaces",
1025            "description": "List all namespaces with their memory counts",
1026            "inputSchema": { "type": "object", "properties": {} }
1027        }),
1028        json!({
1029            "name": "namespace_stats",
1030            "description": "Get detailed statistics for a specific namespace: count, avg importance/confidence, type distribution, tag frequency, date range",
1031            "inputSchema": {
1032                "type": "object",
1033                "properties": {
1034                    "namespace": { "type": "string", "description": "Namespace to get stats for" }
1035                },
1036                "required": ["namespace"]
1037            }
1038        }),
1039        json!({
1040            "name": "delete_namespace",
1041            "description": "Delete all memories in a namespace (destructive, requires confirmation)",
1042            "inputSchema": {
1043                "type": "object",
1044                "properties": {
1045                    "namespace": { "type": "string", "description": "Namespace to delete" },
1046                    "confirm": { "type": "boolean", "description": "Must be true to confirm deletion" }
1047                },
1048                "required": ["namespace", "confirm"]
1049            }
1050        }),
1051        // ── Impact-Aware Recall & Decision Chain Tools ──────────────────────
1052        json!({
1053            "name": "recall_with_impact",
1054            "description": "Semantic search with PageRank-enriched impact data. Returns memories with pagerank, centrality, connected decisions, dependent files, and modification counts.",
1055            "inputSchema": {
1056                "type": "object",
1057                "properties": {
1058                    "query": { "type": "string", "description": "Natural language search query" },
1059                    "k": { "type": "integer", "default": 10, "description": "Number of results" },
1060                    "namespace": { "type": "string", "description": "Filter results to a specific namespace" }
1061                },
1062                "required": ["query"]
1063            }
1064        }),
1065        json!({
1066            "name": "get_decision_chain",
1067            "description": "Follow the evolution of decisions through the knowledge graph. Traces EVOLVED_INTO, LEADS_TO, and DERIVED_FROM edges to build a chronologically ordered decision chain.",
1068            "inputSchema": {
1069                "type": "object",
1070                "properties": {
1071                    "file_path": { "type": "string", "description": "File path to find decisions about (e.g. 'src/auth.rs')" },
1072                    "topic": { "type": "string", "description": "Topic to find decisions about (e.g. 'authentication')" }
1073                }
1074            }
1075        }),
1076        // ── Consolidation Tools ─────────────────────────────────────────────
1077        json!({
1078            "name": "consolidate_decay",
1079            "description": "Run decay consolidation: reduce importance by 10% for memories not accessed within threshold_days",
1080            "inputSchema": {
1081                "type": "object",
1082                "properties": {
1083                    "threshold_days": { "type": "integer", "default": 30, "description": "Memories not accessed in this many days will decay (default: 30)" }
1084                }
1085            }
1086        }),
1087        json!({
1088            "name": "consolidate_creative",
1089            "description": "Run creative consolidation: find pairs of memories with overlapping tags but different types, create RELATES_TO edges between them",
1090            "inputSchema": {
1091                "type": "object",
1092                "properties": {}
1093            }
1094        }),
1095        json!({
1096            "name": "consolidate_cluster",
1097            "description": "Run cluster consolidation: group memories by content_hash prefix, keep highest-importance per group, delete duplicates",
1098            "inputSchema": {
1099                "type": "object",
1100                "properties": {
1101                    "similarity_threshold": { "type": "number", "minimum": 0.5, "maximum": 1.0, "default": 0.92, "description": "Cosine similarity threshold for semantic deduplication (default: 0.92)" }
1102                }
1103            }
1104        }),
1105        json!({
1106            "name": "consolidate_forget",
1107            "description": "Run forget consolidation: delete memories with importance below threshold. Optionally target specific tags for cleanup.",
1108            "inputSchema": {
1109                "type": "object",
1110                "properties": {
1111                    "importance_threshold": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.1, "description": "Delete memories with importance below this value (default: 0.1)" },
1112                    "target_tags": { "type": "array", "items": { "type": "string" }, "description": "Only forget memories with any of these tags (e.g. [\"static-analysis\"])" },
1113                    "max_access_count": { "type": "integer", "default": 0, "description": "Only forget memories accessed at most this many times (default: 0)" }
1114                }
1115            }
1116        }),
1117        json!({
1118            "name": "consolidation_status",
1119            "description": "Show the last run timestamp and affected count for each consolidation cycle type",
1120            "inputSchema": {
1121                "type": "object",
1122                "properties": {}
1123            }
1124        }),
1125        json!({
1126            "name": "detect_patterns",
1127            "description": "Detect cross-session patterns in stored memories. Analyzes repeated searches, file hotspots, decision chains, and tool usage preferences across sessions.",
1128            "inputSchema": {
1129                "type": "object",
1130                "properties": {
1131                    "min_frequency": {
1132                        "type": "integer",
1133                        "minimum": 1,
1134                        "default": 3,
1135                        "description": "Minimum number of occurrences before a pattern is flagged (default: 3)"
1136                    },
1137                    "namespace": {
1138                        "type": "string",
1139                        "description": "Optional namespace to scope the pattern detection"
1140                    }
1141                }
1142            }
1143        }),
1144        json!({
1145            "name": "pattern_insights",
1146            "description": "Generate human-readable markdown insights from cross-session patterns. Summarizes file hotspots, repeated searches, decision chains, and tool preferences.",
1147            "inputSchema": {
1148                "type": "object",
1149                "properties": {
1150                    "min_frequency": {
1151                        "type": "integer",
1152                        "minimum": 1,
1153                        "default": 2,
1154                        "description": "Minimum number of occurrences before a pattern is included (default: 2)"
1155                    },
1156                    "namespace": {
1157                        "type": "string",
1158                        "description": "Optional namespace to scope the pattern insights"
1159                    }
1160                }
1161            }
1162        }),
1163        // ── Memory Refinement & Merge Tools ──────────────────────────────────
1164        json!({
1165            "name": "refine_memory",
1166            "description": "Refine an existing memory: creates a new version linked via EVOLVED_INTO edge, preserving the original for provenance tracking",
1167            "inputSchema": {
1168                "type": "object",
1169                "properties": {
1170                    "id": { "type": "string", "description": "ID of the memory to refine" },
1171                    "content": { "type": "string", "description": "Updated content (optional, inherits from original)" },
1172                    "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
1173                    "tags": { "type": "array", "items": { "type": "string" } }
1174                },
1175                "required": ["id"]
1176            }
1177        }),
1178        json!({
1179            "name": "split_memory",
1180            "description": "Split a memory into multiple parts, each linked to the original via PART_OF edges for provenance tracking",
1181            "inputSchema": {
1182                "type": "object",
1183                "properties": {
1184                    "id": { "type": "string", "description": "ID of the memory to split" },
1185                    "parts": {
1186                        "type": "array",
1187                        "items": {
1188                            "type": "object",
1189                            "properties": {
1190                                "content": { "type": "string" },
1191                                "tags": { "type": "array", "items": { "type": "string" } },
1192                                "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0 }
1193                            },
1194                            "required": ["content"]
1195                        },
1196                        "description": "Array of parts to create from the source memory"
1197                    }
1198                },
1199                "required": ["id", "parts"]
1200            }
1201        }),
1202        json!({
1203            "name": "merge_memories",
1204            "description": "Merge multiple memories into a single summary memory linked via SUMMARIZES edges for provenance tracking",
1205            "inputSchema": {
1206                "type": "object",
1207                "properties": {
1208                    "source_ids": {
1209                        "type": "array",
1210                        "items": { "type": "string" },
1211                        "minItems": 2,
1212                        "description": "IDs of memories to merge (minimum 2)"
1213                    },
1214                    "content": { "type": "string", "description": "Content for the merged summary memory" },
1215                    "memory_type": {
1216                        "type": "string",
1217                        "enum": ["decision", "pattern", "preference", "style", "habit", "insight", "context"],
1218                        "description": "Type for the merged memory (default: insight)"
1219                    },
1220                    "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.7 },
1221                    "tags": { "type": "array", "items": { "type": "string" } }
1222                },
1223                "required": ["source_ids", "content"]
1224            }
1225        }),
1226        json!({
1227            "name": "consolidate_summarize",
1228            "description": "LLM-powered consolidation: find connected components, summarize large clusters into Insight memories linked via SUMMARIZES edges. Requires CODEMEM_COMPRESS_PROVIDER env var.",
1229            "inputSchema": {
1230                "type": "object",
1231                "properties": {
1232                    "cluster_size": { "type": "integer", "minimum": 2, "default": 5, "description": "Minimum cluster size to summarize (default: 5)" }
1233                }
1234            }
1235        }),
1236        json!({
1237            "name": "codemem_metrics",
1238            "description": "Return operational metrics: per-tool latency percentiles (p50/p95/p99), call counters, and gauge values. No parameters required.",
1239            "inputSchema": {
1240                "type": "object",
1241                "properties": {}
1242            }
1243        }),
1244        // ── Enrichment Tools ──────────────────────────────────────────────────
1245        json!({
1246            "name": "enrich_git_history",
1247            "description": "Enrich the knowledge graph with git history: annotate file nodes with commit counts, authors, and churn rate; create CoChanged edges between files that change together; store activity Insights.",
1248            "inputSchema": {
1249                "type": "object",
1250                "properties": {
1251                    "path": { "type": "string", "description": "Absolute path to the git repository root" },
1252                    "days": { "type": "integer", "default": 90, "description": "Number of days of history to analyze (default: 90)" },
1253                    "namespace": { "type": "string", "description": "Namespace for stored insights" }
1254                },
1255                "required": ["path"]
1256            }
1257        }),
1258        json!({
1259            "name": "enrich_security",
1260            "description": "Scan the knowledge graph for security-sensitive files, endpoints, and functions. Annotates nodes with security flags and stores security Insights.",
1261            "inputSchema": {
1262                "type": "object",
1263                "properties": {
1264                    "namespace": { "type": "string", "description": "Namespace filter for insights" }
1265                }
1266            }
1267        }),
1268        json!({
1269            "name": "enrich_performance",
1270            "description": "Analyze graph coupling, dependency depth, critical path (PageRank), and file complexity. Annotates nodes and stores performance Insights.",
1271            "inputSchema": {
1272                "type": "object",
1273                "properties": {
1274                    "namespace": { "type": "string", "description": "Namespace filter for insights" },
1275                    "top": { "type": "integer", "default": 10, "description": "Number of top items to report (default: 10)" }
1276                }
1277            }
1278        }),
1279        // ── Session Checkpoint Tool ─────────────────────────────────────────────
1280        json!({
1281            "name": "session_checkpoint",
1282            "description": "Mid-session checkpoint: summarize activity so far, detect session-scoped and cross-session patterns, identify focus areas, and store new pattern insights. Returns a markdown progress report.",
1283            "inputSchema": {
1284                "type": "object",
1285                "properties": {
1286                    "session_id": { "type": "string", "description": "The current session ID" },
1287                    "namespace": { "type": "string", "description": "Optional namespace to scope pattern detection" }
1288                },
1289                "required": ["session_id"]
1290            }
1291        }),
1292    ]
1293}
1294
1295// ── Tests ───────────────────────────────────────────────────────────────────
1296
1297#[cfg(test)]
1298#[path = "tests/lib_tests.rs"]
1299mod tests;