1use 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
47pub use types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, ToolContent, ToolResult};
49
50pub use bm25::Bm25Index;
52
53use scoring::write_response;
54use types::IndexCache;
55
56pub 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 pub(crate) embeddings: Option<Mutex<Box<dyn codemem_embeddings::EmbeddingProvider>>>,
68 pub(crate) db_path: Option<PathBuf>,
70 pub(crate) index_cache: Mutex<Option<IndexCache>>,
72 pub(crate) scoring_weights: RwLock<ScoringWeights>,
74 pub(crate) bm25_index: Mutex<bm25::Bm25Index>,
77 #[allow(dead_code)]
79 pub(crate) config: codemem_core::CodememConfig,
80 pub(crate) metrics: std::sync::Arc<metrics::InMemoryMetrics>,
82}
83
84impl McpServer {
85 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 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 let index_path = db_path.with_extension("idx");
116 if index_path.exists() {
117 vector.load(&index_path)?;
118 }
119
120 let graph = GraphEngine::from_storage(&storage)?;
122
123 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 server.lock_graph()?.recompute_centrality();
131
132 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 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 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 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 ctx.push_str(&format!("[{}]", memory_type));
233
234 if let Some(ns) = namespace {
236 ctx.push_str(&format!(" [namespace:{}]", ns));
237 }
238
239 if !tags.is_empty() {
241 ctx.push_str(&format!(" [tags:{}]", tags.join(",")));
242 }
243
244 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 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 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 ctx.push_str(&format!("[{} {}]", sym.visibility, sym.kind));
289
290 ctx.push_str(&format!(" File: {}", sym.file_path));
292
293 if let Some(ref parent) = sym.parent {
295 ctx.push_str(&format!(" Parent: {}", parent));
296 }
297
298 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 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 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 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 let mut candidates: Vec<String> = Vec::new();
371
372 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 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 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 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 pub fn storage(&self) -> &dyn StorageBackend {
434 &*self.storage
435 }
436
437 pub fn graph(&self) -> &Mutex<GraphEngine> {
439 &self.graph
440 }
441
442 pub fn vector(&self) -> &Mutex<HnswIndex> {
444 &self.vector
445 }
446
447 pub fn embeddings(&self) -> Option<&Mutex<Box<dyn codemem_embeddings::EmbeddingProvider>>> {
449 self.embeddings.as_ref()
450 }
451
452 pub fn bm25(&self) -> &Mutex<bm25::Bm25Index> {
454 &self.bm25_index
455 }
456
457 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 pub fn db_path(&self) -> Option<&Path> {
471 self.db_path.as_deref()
472 }
473
474 pub fn config(&self) -> &codemem_core::CodememConfig {
476 &self.config
477 }
478
479 pub fn metrics_collector(&self) -> &std::sync::Arc<metrics::InMemoryMetrics> {
481 &self.metrics
482 }
483
484 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 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 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
642pub struct StdioTransport<'a> {
647 server: &'a McpServer,
648}
649
650impl<'a> StdioTransport<'a> {
651 pub fn new(server: &'a McpServer) -> Self {
653 Self { server }
654 }
655
656 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 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
695fn 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 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 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 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 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 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 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 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 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#[cfg(test)]
1298#[path = "tests/lib_tests.rs"]
1299mod tests;