Skip to main content

semantic_memory_mcp/
server.rs

1//! MCP server handler using rmcp's #[tool_router] macro.
2//!
3//! Each #[tool] method becomes an MCP tool that Hermes/Claude Desktop
4//! can discover and call. The rmcp macro auto-generates JSON Schema
5//! from the parameter structs in tools.rs.
6
7use crate::bridge::MemoryBridge;
8use crate::tools::*;
9use rmcp::{
10    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
11    tool, tool_handler, tool_router, ErrorData, ServerHandler,
12};
13use std::collections::HashSet;
14use std::sync::Arc;
15use tokio::runtime::Handle;
16
17// Re-export the specific parameter types we use in tool signatures.
18use crate::tools::{
19    AddGraphEdgeParams, CommunityParams, FactorGraphParams, InvalidateGraphEdgeParams,
20    ListGraphEdgesParams, RecordOutcomeParams, TopologyParams,
21};
22
23pub struct SemanticMemoryServer {
24    bridge: Arc<MemoryBridge>,
25    tool_router: ToolRouter<Self>,
26}
27
28impl SemanticMemoryServer {
29    pub fn new(bridge: MemoryBridge) -> Self {
30        Self {
31            bridge: Arc::new(bridge),
32            tool_router: Self::tool_router(),
33        }
34    }
35}
36
37/// Helper: load all stored graph edges from the store as GraphEdgeRef tuples
38/// for discord scoring.
39fn load_stored_edge_refs(
40    store: &semantic_memory::MemoryStore,
41) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
42    let edges =
43        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
44            .map_err(|e| {
45                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
46            })?;
47    let refs = edges
48        .iter()
49        .map(|edge| {
50            let parsed_type = edge
51                .edge_type_parsed
52                .clone()
53                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
54                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
55                    relation: "unknown".to_string(),
56                });
57            let type_str = match parsed_type {
58                semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
59                semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
60                semantic_memory::GraphEdgeType::Causal { .. } => "causal",
61                semantic_memory::GraphEdgeType::Entity { .. } => "entity",
62            };
63            semantic_memory::discord::GraphEdgeRef {
64                source: edge.source.clone(),
65                target: edge.target.clone(),
66                edge_type: type_str.to_string(),
67                weight: edge.weight,
68            }
69        })
70        .collect();
71    Ok(refs)
72}
73
74/// Helper: load all stored graph edges from the store as raw factor graph
75/// edge tuples (source, target, GraphEdgeType, weight, metadata_json).
76fn load_stored_factor_edges(
77    store: &semantic_memory::MemoryStore,
78) -> Result<
79    Vec<(
80        String,
81        String,
82        semantic_memory::GraphEdgeType,
83        f64,
84        Option<String>,
85    )>,
86    ErrorData,
87> {
88    let edges =
89        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
90            .map_err(|e| {
91                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
92            })?;
93    let raw = edges
94        .iter()
95        .map(|edge| {
96            let parsed_type = edge
97                .edge_type_parsed
98                .clone()
99                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
100                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
101                    relation: "unknown".to_string(),
102                });
103            (
104                edge.source.clone(),
105                edge.target.clone(),
106                parsed_type,
107                edge.weight,
108                edge.metadata.clone(),
109            )
110        })
111        .collect();
112    Ok(raw)
113}
114
115/// Helper: load all stored graph edges as (source, target) pairs.
116fn load_stored_edge_pairs(
117    store: &semantic_memory::MemoryStore,
118) -> Result<Vec<(String, String)>, ErrorData> {
119    let edges =
120        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
121            .map_err(|e| {
122                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
123            })?;
124    let pairs = edges
125        .iter()
126        .map(|edge| (edge.source.clone(), edge.target.clone()))
127        .collect();
128    Ok(pairs)
129}
130
131/// Helper: load graph edges for a neighborhood around the given seed node IDs.
132/// Uses BFS expansion with max_hops=2 and max_nodes=200 by default.
133/// Falls back to full graph load if seeds are empty.
134fn load_neighborhood_edge_pairs(
135    store: &semantic_memory::MemoryStore,
136    seed_ids: &[String],
137) -> Result<Vec<(String, String)>, ErrorData> {
138    if seed_ids.is_empty() {
139        return load_stored_edge_pairs(store);
140    }
141    let edges = tokio::task::block_in_place(|| {
142        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
143            seed_ids.to_vec(),
144            2,
145            200,
146        ))
147    })
148    .map_err(|e| {
149        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
150    })?;
151    let pairs = edges
152        .iter()
153        .map(|edge| (edge.source.clone(), edge.target.clone()))
154        .collect();
155    Ok(pairs)
156}
157
158/// Helper: load graph edges for a neighborhood as GraphEdgeRef vec.
159fn load_neighborhood_edge_refs(
160    store: &semantic_memory::MemoryStore,
161    seed_ids: &[String],
162) -> Result<Vec<semantic_memory::discord::GraphEdgeRef>, ErrorData> {
163    if seed_ids.is_empty() {
164        return load_stored_edge_refs(store);
165    }
166    let edges = tokio::task::block_in_place(|| {
167        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
168            seed_ids.to_vec(),
169            2,
170            200,
171        ))
172    })
173    .map_err(|e| {
174        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
175    })?;
176    let refs = edges
177        .iter()
178        .map(|edge| {
179            let parsed_type = edge
180                .edge_type_parsed
181                .clone()
182                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
183                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
184                    relation: "unknown".to_string(),
185                });
186            let type_str = match parsed_type {
187                semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
188                semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
189                semantic_memory::GraphEdgeType::Causal { .. } => "causal",
190                semantic_memory::GraphEdgeType::Entity { .. } => "entity",
191            };
192            semantic_memory::discord::GraphEdgeRef {
193                source: edge.source.clone(),
194                target: edge.target.clone(),
195                edge_type: type_str.to_string(),
196                weight: edge.weight,
197            }
198        })
199        .collect();
200    Ok(refs)
201}
202
203/// Helper: load graph edges for a neighborhood as factor graph tuples.
204fn load_neighborhood_factor_edges(
205    store: &semantic_memory::MemoryStore,
206    seed_ids: &[String],
207) -> Result<
208    Vec<(
209        String,
210        String,
211        semantic_memory::GraphEdgeType,
212        f64,
213        Option<String>,
214    )>,
215    ErrorData,
216> {
217    if seed_ids.is_empty() {
218        return load_stored_factor_edges(store);
219    }
220    let edges = tokio::task::block_in_place(|| {
221        Handle::current().block_on(store.list_graph_edges_for_neighborhood(
222            seed_ids.to_vec(),
223            2,
224            200,
225        ))
226    })
227    .map_err(|e| {
228        ErrorData::internal_error(format!("Failed to load neighborhood edges: {e}"), None)
229    })?;
230    let raw = edges
231        .iter()
232        .map(|edge| {
233            let parsed_type = edge
234                .edge_type_parsed
235                .clone()
236                .or_else(|| serde_json::from_str(&edge.edge_type).ok())
237                .unwrap_or(semantic_memory::GraphEdgeType::Entity {
238                    relation: "unknown".to_string(),
239                });
240            (
241                edge.source.clone(),
242                edge.target.clone(),
243                parsed_type,
244                edge.weight,
245                edge.metadata.clone(),
246            )
247        })
248        .collect();
249    Ok(raw)
250}
251
252/// Load fact ids targeted by entity relation="supersedes" graph edges.
253fn load_superseded_targets(
254    store: &semantic_memory::MemoryStore,
255) -> Result<HashSet<String>, ErrorData> {
256    let edges =
257        tokio::task::block_in_place(|| Handle::current().block_on(store.list_all_graph_edges()))
258            .map_err(|e| {
259                ErrorData::internal_error(format!("Failed to load graph edges: {e}"), None)
260            })?;
261    let mut targets = HashSet::new();
262    for edge in edges {
263        let parsed_type = edge
264            .edge_type_parsed
265            .clone()
266            .or_else(|| serde_json::from_str(&edge.edge_type).ok());
267        if let Some(semantic_memory::GraphEdgeType::Entity { relation }) = parsed_type {
268            if relation == "supersedes" {
269                targets.insert(edge.target);
270            }
271        }
272    }
273    Ok(targets)
274}
275
276fn query_allows_superseded(query: &str) -> bool {
277    let q = query.to_lowercase();
278    q.contains("supersed")
279        || q.contains("stale")
280        || q.contains("obsolete")
281        || q.contains("histor")
282        || q.contains("old fact")
283        || q.contains("previous fact")
284}
285
286/// Serialize a JSON value to a pretty string, mapping serialization errors
287/// to protocol-level errors instead of success strings.
288fn json_to_string(value: &serde_json::Value) -> Result<String, ErrorData> {
289    serde_json::to_string_pretty(value)
290        .map_err(|e| ErrorData::internal_error(format!("Serialization error: {e}"), None))
291}
292
293#[tool_router]
294impl SemanticMemoryServer {
295    // ── Core search tools ────────────────────────────────────────────
296
297    #[tool(
298        description = "Semantic hybrid search (BM25 + vector + RRF). Returns ranked results with content, scores, and stable result IDs.",
299        annotations(read_only_hint = true)
300    )]
301    fn sm_search(
302        &self,
303        Parameters(SearchParams {
304            query,
305            top_k,
306            namespaces,
307        }): Parameters<SearchParams>,
308    ) -> Result<String, ErrorData> {
309        let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
310        let allow_superseded = query_allows_superseded(&query);
311        let search_k = if allow_superseded {
312            requested_k
313        } else {
314            (requested_k * 4).max(20)
315        };
316        let ns: Option<Vec<&str>> = namespaces
317            .as_ref()
318            .map(|v| v.iter().map(|s| s.as_str()).collect());
319
320        let store = &self.bridge.store;
321        let result = tokio::task::block_in_place(|| {
322            Handle::current().block_on(store.search(&query, Some(search_k), ns.as_deref(), None))
323        });
324
325        match result {
326            Ok(results) => {
327                let superseded_targets = if allow_superseded {
328                    HashSet::new()
329                } else {
330                    load_superseded_targets(store)?
331                };
332                let fresh_results: Vec<_> = results
333                    .iter()
334                    .filter(|r| !superseded_targets.contains(&r.source.result_id()))
335                    .collect();
336                let result_refs: Vec<_> =
337                    if superseded_targets.is_empty() || fresh_results.is_empty() {
338                        results.iter().collect()
339                    } else {
340                        fresh_results
341                    };
342                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
343                let json_results: Vec<serde_json::Value> = result_refs
344                    .iter()
345                    .take(requested_k)
346                    .map(|r| {
347                        serde_json::json!({
348                            "result_id": r.source.result_id(),
349                            "content": r.content,
350                            "source": format!("{:?}", r.source),
351                            "score": r.score,
352                            "bm25_rank": r.bm25_rank,
353                            "vector_rank": r.vector_rank,
354                            "cosine_similarity": r.cosine_similarity,
355                        })
356                    })
357                    .collect();
358                json_to_string(&serde_json::json!({
359                    "ok": true,
360                    "results": json_results,
361                    "count": json_results.len(),
362                    "superseded_filtered_count": superseded_filtered_count,
363                }))
364            }
365            Err(e) => Err(ErrorData::internal_error(
366                format!("Search error: {e}"),
367                None,
368            )),
369        }
370    }
371
372    // DEPRECATED #[tool(
373        // description = "Search with full score breakdown showing how BM25 and vector scores combine. Useful for debugging retrieval quality.",
374        // annotations(read_only_hint = true)
375    // )]
376    #[allow(dead_code)]
377    fn sm_search_explained(
378        &self,
379        Parameters(SearchExplainedParams { query, top_k }): Parameters<SearchExplainedParams>,
380    ) -> Result<String, ErrorData> {
381        let requested_k = top_k.map(|v| v as usize).unwrap_or(5);
382        let allow_superseded = query_allows_superseded(&query);
383        let search_k = if allow_superseded {
384            requested_k
385        } else {
386            (requested_k * 4).max(20)
387        };
388        let store = &self.bridge.store;
389        let result = tokio::task::block_in_place(|| {
390            Handle::current().block_on(store.search_explained(&query, Some(search_k), None, None))
391        });
392
393        match result {
394            Ok(results) => {
395                let superseded_targets = if allow_superseded {
396                    HashSet::new()
397                } else {
398                    load_superseded_targets(store)?
399                };
400                let fresh_results: Vec<_> = results
401                    .iter()
402                    .filter(|r| !superseded_targets.contains(&r.result.source.result_id()))
403                    .collect();
404                let result_refs: Vec<_> =
405                    if superseded_targets.is_empty() || fresh_results.is_empty() {
406                        results.iter().collect()
407                    } else {
408                        fresh_results
409                    };
410                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
411                let json_results: Vec<serde_json::Value> = result_refs
412                    .iter()
413                    .take(requested_k)
414                    .map(|r| {
415                        serde_json::json!({
416                            "result_id": r.result.source.result_id(),
417                            "content": r.result.content,
418                            "source": format!("{:?}", r.result.source),
419                            "score": r.result.score,
420                            "bm25_rank": r.result.bm25_rank,
421                            "vector_rank": r.result.vector_rank,
422                            "cosine_similarity": r.result.cosine_similarity,
423                            "breakdown": {
424                                "rrf_score": r.breakdown.rrf_score,
425                                "bm25_score": r.breakdown.bm25_score,
426                                "vector_score": r.breakdown.vector_score,
427                                "recency_score": r.breakdown.recency_score,
428                                "bm25_rank": r.breakdown.bm25_rank,
429                                "vector_rank": r.breakdown.vector_rank,
430                                "vector_source_rank": r.breakdown.vector_source_rank,
431                                "vector_source_score": r.breakdown.vector_source_score,
432                                "bm25_contribution": r.breakdown.bm25_contribution,
433                                "vector_contribution": r.breakdown.vector_contribution,
434                                "vector_reranked_from_f32": r.breakdown.vector_reranked_from_f32,
435                                "bm25_weight": r.breakdown.bm25_weight,
436                                "vector_weight": r.breakdown.vector_weight,
437                                "recency_weight": r.breakdown.recency_weight,
438                                "rrf_k": r.breakdown.rrf_k,
439                            },
440                        })
441                    })
442                    .collect();
443                json_to_string(&serde_json::json!({
444                    "ok": true,
445                    "results": json_results,
446                    "count": json_results.len(),
447                    "superseded_filtered_count": superseded_filtered_count,
448                }))
449            }
450            Err(e) => Err(ErrorData::internal_error(
451                format!("Search error: {e}"),
452                None,
453            )),
454        }
455    }
456
457    #[tool(
458        description = "Add a fact to the knowledge base. Embedded and indexed for semantic search. Returns fact ID and content digest.",
459        annotations(idempotent_hint = true)
460    )]
461    fn sm_add_fact(
462        &self,
463        Parameters(AddFactParams {
464            content,
465            namespace,
466            source,
467            extract_entities,
468            memory_kind,
469            sensitivity,
470            evidence_refs,
471        }): Parameters<AddFactParams>,
472    ) -> Result<String, ErrorData> {
473        let store = &self.bridge.store;
474        let src = source.as_deref();
475
476        // Admission gate: classify sensitivity
477        let sens = sensitivity.unwrap_or_else(|| "internal".to_string());
478        let kind = memory_kind.unwrap_or_else(|| "durable_fact".to_string());
479
480        // Block confidential/restricted content from autocapture
481        if sens == "confidential" || sens == "restricted" {
482            return Err(ErrorData::invalid_params(
483                format!("Admission gate BLOCKED: sensitivity='{sens}' content cannot be stored without explicit user request"),
484                None,
485            ));
486        }
487
488        // Block ephemeral_inference from becoming durable without evidence
489        if kind == "ephemeral_inference" {
490            let refs = evidence_refs.as_ref().map(|v| v.len()).unwrap_or(0);
491            if refs == 0 {
492                return Err(ErrorData::invalid_params(
493                    "Admission gate BLOCKED: ephemeral_inference requires evidence_refs to promote to durable".to_string(),
494                    None,
495                ));
496            }
497        }
498
499        // Build metadata JSON with typed memory fields
500        let mut meta = serde_json::Map::new();
501        meta.insert("memory_kind".to_string(), serde_json::json!(kind));
502        meta.insert("sensitivity".to_string(), serde_json::json!(sens));
503        if let Some(refs) = evidence_refs {
504            meta.insert("evidence_refs".to_string(), serde_json::json!(refs));
505        }
506        let _metadata_str = serde_json::to_string(&serde_json::Value::Object(meta)).ok();
507
508        let result = tokio::task::block_in_place(|| {
509            Handle::current().block_on(store.add_fact(&namespace, &content, src, None))
510        });
511
512        match result {
513            Ok(id) => {
514                // Optional entity extraction — best-effort, never fails the whole operation.
515                if extract_entities == Some(true) {
516                    let prompt = format!(
517                        "Extract entities from this text as JSON. Format: {{\"entities\": [{{\"name\": \"...\", \"type\": \"person|project|concept|tool|version|path\"}}]}}\nText: {content}\nJSON:"
518                    );
519                    let body = serde_json::json!({
520                        "model": "granite4.1:3b",
521                        "prompt": prompt,
522                        "stream": false,
523                        "options": {"temperature": 0, "num_predict": 200}
524                    });
525                    if let Ok(resp) = reqwest::blocking::Client::new()
526                        .post("http://127.0.0.1:11434/api/generate")
527                        .json(&body)
528                        .send()
529                    {
530                        if let Ok(v) = resp.json::<serde_json::Value>() {
531                            if let Some(response_str) =
532                                v.get("response").and_then(|r| r.as_str())
533                            {
534                                // Use boundary compiler for robust JSON parsing with duplicate-key rejection
535                                let parsed_result = boundary_compiler::parse_with_dup_check(response_str.trim());
536                                if let Ok(parsed) = parsed_result {
537                                    if let Some(entities) =
538                                        parsed.get("entities").and_then(|e| e.as_array())
539                                    {
540                                        let fact_node = format!("fact:{id}");
541                                        for entity in entities {
542                                            if let Some(name) =
543                                                entity.get("name").and_then(|n| n.as_str())
544                                            {
545                                                let entity_node = format!("entity:{name}");
546                                                let _ = tokio::task::block_in_place(|| {
547                                                    Handle::current().block_on(
548                                                        store.add_graph_edge(
549                                                            &fact_node,
550                                                            &entity_node,
551                                                            semantic_memory::GraphEdgeType::Entity {
552                                                                relation: "mentions".to_string(),
553                                                            },
554                                                            1.0,
555                                                            None,
556                                                        ),
557                                                    )
558                                                });
559                                            }
560                                        }
561                                    }
562                                }
563                            }
564                        }
565                    }
566                }
567
568                json_to_string(&serde_json::json!({
569                    "ok": true,
570                    "fact_id": id,
571                    "namespace": namespace,
572                    "message": "Fact added successfully",
573                }))
574            }
575            Err(e) => Err(ErrorData::internal_error(
576                format!("Error adding fact: {e}"),
577                None,
578            )),
579        }
580    }
581
582    #[tool(
583        description = "Ingest a document with automatic chunking. Splits into chunks, each embedded and indexed. Returns document ID and chunk count.",
584        annotations(idempotent_hint = true)
585    )]
586    fn sm_ingest_document(
587        &self,
588        Parameters(IngestDocumentParams {
589            content,
590            title,
591            namespace,
592        }): Parameters<IngestDocumentParams>,
593    ) -> Result<String, ErrorData> {
594        let store = &self.bridge.store;
595        let result = tokio::task::block_in_place(|| {
596            Handle::current()
597                .block_on(store.ingest_document(&title, &content, &namespace, None, None))
598        });
599
600        match result {
601            Ok(doc_id) => {
602                let chunk_count = tokio::task::block_in_place(|| {
603                    Handle::current().block_on(store.count_chunks_for_document(&doc_id))
604                })
605                .unwrap_or(0);
606                json_to_string(&serde_json::json!({
607                    "ok": true,
608                    "document_id": doc_id,
609                    "title": title,
610                    "chunk_count": chunk_count,
611                    "message": "Document ingested successfully",
612                }))
613            }
614            Err(e) => Err(ErrorData::internal_error(
615                format!("Error ingesting document: {e}"),
616                None,
617            )),
618        }
619    }
620
621    #[tool(
622        description = "Get knowledge base statistics: fact/chunk/document/session counts, DB size, embedding model, and graph edge count.",
623        annotations(read_only_hint = true)
624    )]
625    fn sm_stats(&self) -> Result<String, ErrorData> {
626        let store = &self.bridge.store;
627        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.stats()));
628
629        match result {
630            Ok(stats) => {
631                // Load graph edge count separately — propagates errors
632                // instead of hiding them (SM-AUD-016).
633                let graph_edge_count = tokio::task::block_in_place(|| {
634                    Handle::current().block_on(store.list_all_graph_edges())
635                })
636                .map(|edges| edges.len())
637                .unwrap_or_else(|e| {
638                    tracing::warn!("graph_edges table unavailable: {e}");
639                    0
640                });
641                json_to_string(&serde_json::json!({
642                    "ok": true,
643                    "facts": stats.total_facts,
644                    "chunks": stats.total_chunks,
645                    "documents": stats.total_documents,
646                    "sessions": stats.total_sessions,
647                    "messages": stats.total_messages,
648                    "graph_edges": graph_edge_count,
649                    "db_size_bytes": stats.database_size_bytes,
650                    "db_size_mb": (stats.database_size_bytes as f64 / 1_048_576.0 * 100.0).round() / 100.0,
651                    "embedding_model": stats.embedding_model,
652                    "embedding_dimensions": stats.embedding_dimensions,
653                }))
654            }
655            Err(e) => Err(ErrorData::internal_error(format!("Stats error: {e}"), None)),
656        }
657    }
658
659    #[tool(
660        description = "Find shortest path between two items in the knowledge graph. Traverses all edge types. Returns node IDs with edge evidence per hop.",
661        annotations(read_only_hint = true)
662    )]
663    fn sm_graph_path(
664        &self,
665        Parameters(GraphPathParams {
666            from_id,
667            to_id,
668            max_depth,
669        }): Parameters<GraphPathParams>,
670    ) -> Result<String, ErrorData> {
671        let depth = max_depth.map(|v| v as usize).unwrap_or(5);
672        let store = &self.bridge.store;
673        let g = store.graph_view();
674
675        match g.path(&from_id, &to_id, depth) {
676            Ok(Some(path)) => {
677                // Build edge evidence for each hop by examining neighbors.
678                let path_segments = build_path_segments(store, &path);
679                json_to_string(&serde_json::json!({
680                    "ok": true,
681                    "from": from_id,
682                    "to": to_id,
683                    "path": path,
684                    "path_length": path.len(),
685                    "segments": path_segments,
686                }))
687            }
688            Ok(None) => json_to_string(&serde_json::json!({
689                "ok": true,
690                "from": from_id,
691                "to": to_id,
692                "path": null,
693                "message": format!("No path found from {from_id} to {to_id} within depth {depth}"),
694            })),
695            Err(e) => Err(ErrorData::internal_error(
696                format!("Graph view error: {e}"),
697                None,
698            )),
699        }
700    }
701
702    // ── Direct read and supersession tools (v0.3.1) ──────────────────
703
704    #[tool(
705        description = "Fetch one fact by id (bare UUID or prefixed 'fact:<uuid>'). Returns full content, namespace, source, timestamps, and metadata.",
706        annotations(read_only_hint = true)
707    )]
708    fn sm_get_fact(
709        &self,
710        Parameters(GetFactParams { fact_id }): Parameters<GetFactParams>,
711    ) -> Result<String, ErrorData> {
712        let bare = fact_id
713            .strip_prefix("fact:")
714            .unwrap_or(&fact_id)
715            .to_string();
716        let store = &self.bridge.store;
717        let result =
718            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)));
719        match result {
720            Ok(Some(f)) => json_to_string(&serde_json::json!({
721                "ok": true,
722                "found": true,
723                "fact": {
724                    "result_id": format!("fact:{}", f.id),
725                    "id": f.id,
726                    "namespace": f.namespace,
727                    "content": f.content,
728                    "source": f.source,
729                    "created_at": f.created_at,
730                    "updated_at": f.updated_at,
731                    "metadata": f.metadata,
732                },
733            })),
734            Ok(None) => json_to_string(&serde_json::json!({
735                "ok": true,
736                "found": false,
737                "message": format!("No fact with id '{fact_id}'"),
738            })),
739            Err(e) => Err(ErrorData::internal_error(
740                format!("get_fact error: {e}"),
741                None,
742            )),
743        }
744    }
745
746    #[tool(
747        description = "Enumerate facts in a namespace (newest first) with pagination. Exhaustive, not similarity-ranked — for browsing, auditing, or deduping.",
748        annotations(read_only_hint = true)
749    )]
750    fn sm_list_facts(
751        &self,
752        Parameters(ListFactsParams {
753            namespace,
754            limit,
755            offset,
756        }): Parameters<ListFactsParams>,
757    ) -> Result<String, ErrorData> {
758        let lim = limit.map(|v| v as usize).unwrap_or(50);
759        let off = offset.map(|v| v as usize).unwrap_or(0);
760        let store = &self.bridge.store;
761        let result = tokio::task::block_in_place(|| {
762            Handle::current().block_on(store.list_facts(&namespace, lim, off))
763        });
764        match result {
765            Ok(facts) => {
766                let arr: Vec<serde_json::Value> = facts
767                    .iter()
768                    .map(|f| {
769                        serde_json::json!({
770                            "result_id": format!("fact:{}", f.id),
771                            "id": f.id,
772                            "namespace": f.namespace,
773                            "content": f.content,
774                            "source": f.source,
775                            "updated_at": f.updated_at,
776                        })
777                    })
778                    .collect();
779                json_to_string(&serde_json::json!({
780                    "ok": true,
781                    "namespace": namespace,
782                    "count": arr.len(),
783                    "limit": lim,
784                    "offset": off,
785                    "facts": arr,
786                }))
787            }
788            Err(e) => Err(ErrorData::internal_error(
789                format!("list_facts error: {e}"),
790                None,
791            )),
792        }
793    }
794
795    #[tool(
796        description = "List namespaces that currently contain facts. Use before sm_list_facts to discover what is stored.",
797        annotations(read_only_hint = true)
798    )]
799    fn sm_list_namespaces(&self) -> Result<String, ErrorData> {
800        let store = &self.bridge.store;
801        let result = tokio::task::block_in_place(|| {
802            Handle::current().block_on(store.list_fact_namespaces())
803        });
804        match result {
805            Ok(ns) => json_to_string(&serde_json::json!({
806                "ok": true,
807                "count": ns.len(),
808                "namespaces": ns,
809            })),
810            Err(e) => Err(ErrorData::internal_error(
811                format!("list_namespaces error: {e}"),
812                None,
813            )),
814        }
815    }
816
817    #[tool(
818        description = "Fetch a fact plus its graph neighbors WITH their content in one call. Hydrates neighbor facts for ids returned by graph tools.",
819        annotations(read_only_hint = true)
820    )]
821    fn sm_get_fact_neighbors(
822        &self,
823        Parameters(GetFactNeighborsParams { item_id }): Parameters<GetFactNeighborsParams>,
824    ) -> Result<String, ErrorData> {
825        let node_id = if item_id.contains(':') {
826            item_id.clone()
827        } else {
828            format!("fact:{item_id}")
829        };
830        let bare = node_id
831            .strip_prefix("fact:")
832            .unwrap_or(&node_id)
833            .to_string();
834        let store = &self.bridge.store;
835
836        let center =
837            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&bare)))
838                .map_err(|e| ErrorData::internal_error(format!("get_fact error: {e}"), None))?;
839        let edges = tokio::task::block_in_place(|| {
840            Handle::current().block_on(store.list_graph_edges_for_node(&node_id))
841        })
842        .map_err(|e| ErrorData::internal_error(format!("list edges error: {e}"), None))?;
843
844        let mut neighbors: Vec<serde_json::Value> = Vec::new();
845        for e in &edges {
846            let outgoing = e.source == node_id;
847            let other = if outgoing { &e.target } else { &e.source };
848            let other_bare = other.strip_prefix("fact:").unwrap_or(other).to_string();
849            let content = tokio::task::block_in_place(|| {
850                Handle::current().block_on(store.get_fact(&other_bare))
851            })
852            .ok()
853            .flatten()
854            .map(|f| f.content);
855            neighbors.push(serde_json::json!({
856                "neighbor_id": other,
857                "direction": if outgoing { "out" } else { "in" },
858                "edge_type": e.edge_type,
859                "weight": e.weight,
860                "content": content,
861            }));
862        }
863        json_to_string(&serde_json::json!({
864            "ok": true,
865            "item_id": node_id,
866            "center_content": center.map(|f| f.content),
867            "neighbor_count": neighbors.len(),
868            "neighbors": neighbors,
869        }))
870    }
871
872    #[tool(
873        description = "Create a replacement fact and link it to a stale fact via 'supersedes' edge. Use instead of deleting outdated facts. Returns new fact id and edge id.",
874        annotations(idempotent_hint = true)
875    )]
876    fn sm_supersede_fact(
877        &self,
878        Parameters(SupersedeFactParams {
879            old_fact_id,
880            content,
881            namespace,
882            source,
883            reason,
884        }): Parameters<SupersedeFactParams>,
885    ) -> Result<String, ErrorData> {
886        use semantic_memory::GraphEdgeType;
887
888        let old_bare = old_fact_id
889            .strip_prefix("fact:")
890            .unwrap_or(&old_fact_id)
891            .to_string();
892        let old_node = format!("fact:{old_bare}");
893        let store = &self.bridge.store;
894        let old =
895            tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&old_bare)))
896                .map_err(|e| ErrorData::internal_error(format!("get old fact error: {e}"), None))?;
897        let Some(old_fact) = old else {
898            return Err(ErrorData::invalid_params(
899                format!("No fact with id '{old_fact_id}'"),
900                None,
901            ));
902        };
903
904        let ns = namespace.unwrap_or_else(|| old_fact.namespace.clone());
905        let new_id = tokio::task::block_in_place(|| {
906            Handle::current().block_on(store.add_fact(&ns, &content, source.as_deref(), None))
907        })
908        .map_err(|e| ErrorData::internal_error(format!("add replacement fact error: {e}"), None))?;
909        let new_node = format!("fact:{new_id}");
910        let metadata = serde_json::json!({
911            "reason": reason.unwrap_or_else(|| "replacement fact supersedes stale fact".to_string()),
912            "old_fact_id": old_bare,
913        });
914        let edge = tokio::task::block_in_place(|| {
915            Handle::current().block_on(store.add_graph_edge(
916                &new_node,
917                &old_node,
918                GraphEdgeType::Entity {
919                    relation: "supersedes".to_string(),
920                },
921                1.0,
922                Some(metadata),
923            ))
924        })
925        .map_err(|e| ErrorData::internal_error(format!("add supersedes edge error: {e}"), None))?;
926
927        json_to_string(&serde_json::json!({
928            "ok": true,
929            "new_fact_id": new_id,
930            "new_result_id": new_node,
931            "old_fact_id": old_bare,
932            "old_result_id": old_node,
933            "namespace": ns,
934            "edge_id": edge.id,
935            "relation": "supersedes",
936        }))
937    }
938
939    // ── Conversation / session tools (v0.3.0) ────────────────────────
940
941    // DEPRECATED #[tool(
942        // description = "Create a conversation session (container for messages). Returns session id. Use to persist history recallable via sm_search_conversations.",
943        // annotations(idempotent_hint = true)
944    // )]
945    #[allow(dead_code)]
946    fn sm_create_session(
947        &self,
948        Parameters(CreateSessionParams { channel, metadata }): Parameters<CreateSessionParams>,
949    ) -> Result<String, ErrorData> {
950        let meta: Option<serde_json::Value> = metadata
951            .as_deref()
952            .and_then(|s| serde_json::from_str(s).ok());
953        let store = &self.bridge.store;
954        let result = tokio::task::block_in_place(|| {
955            Handle::current().block_on(store.create_session_with_metadata(&channel, meta))
956        });
957        match result {
958            Ok(id) => json_to_string(
959                &serde_json::json!({"ok": true, "session_id": id, "channel": channel}),
960            ),
961            Err(e) => Err(ErrorData::internal_error(
962                format!("create_session error: {e}"),
963                None,
964            )),
965        }
966    }
967
968    // DEPRECATED #[tool(
969        // description = "Append a message to a session. role: user|assistant|system|tool. Message is embedded and FTS-indexed. Returns message id."
970    // )]
971    #[allow(dead_code)]
972    fn sm_add_message(
973        &self,
974        Parameters(AddMessageParams {
975            session_id,
976            role,
977            content,
978        }): Parameters<AddMessageParams>,
979    ) -> Result<String, ErrorData> {
980        let parsed_role = match role.to_lowercase().as_str() {
981            "user" => semantic_memory::types::Role::User,
982            "assistant" => semantic_memory::types::Role::Assistant,
983            "system" => semantic_memory::types::Role::System,
984            "tool" => semantic_memory::types::Role::Tool,
985            other => {
986                return Err(ErrorData::invalid_params(
987                    format!("invalid role '{other}' (use user|assistant|system|tool)"),
988                    None,
989                ))
990            }
991        };
992        let store = &self.bridge.store;
993        let result = tokio::task::block_in_place(|| {
994            Handle::current().block_on(store.add_message_embedded(
995                &session_id,
996                parsed_role,
997                &content,
998                None,
999                None,
1000            ))
1001        });
1002        match result {
1003            Ok(id) => json_to_string(
1004                &serde_json::json!({"ok": true, "message_id": id, "session_id": session_id}),
1005            ),
1006            Err(e) => Err(ErrorData::internal_error(
1007                format!("add_message error: {e}"),
1008                None,
1009            )),
1010        }
1011    }
1012
1013    // DEPRECATED #[tool(description = "List recent conversation sessions (newest first) with message counts.", annotations(read_only_hint = true))]
1014    #[allow(dead_code)]
1015    fn sm_list_sessions(
1016        &self,
1017        Parameters(ListSessionsParams { limit, offset }): Parameters<ListSessionsParams>,
1018    ) -> Result<String, ErrorData> {
1019        let lim = limit.map(|v| v as usize).unwrap_or(20);
1020        let off = offset.map(|v| v as usize).unwrap_or(0);
1021        let store = &self.bridge.store;
1022        let result = tokio::task::block_in_place(|| {
1023            Handle::current().block_on(store.list_sessions(lim, off))
1024        });
1025        match result {
1026            Ok(sessions) => json_to_string(&serde_json::json!({
1027                "ok": true,
1028                "count": sessions.len(),
1029                "sessions": sessions.iter().map(|s| serde_json::json!({
1030                    "session_id": s.id,
1031                    "channel": s.channel,
1032                    "message_count": s.message_count,
1033                    "created_at": s.created_at,
1034                    "updated_at": s.updated_at,
1035                })).collect::<Vec<_>>(),
1036            })),
1037            Err(e) => Err(ErrorData::internal_error(
1038                format!("list_sessions error: {e}"),
1039                None,
1040            )),
1041        }
1042    }
1043
1044    // DEPRECATED #[tool(
1045        // description = "Get most recent messages from a session within a token budget (default 4000), chronological order. Returns role, content, timestamps.",
1046        // annotations(read_only_hint = true)
1047    // )]
1048    #[allow(dead_code)]
1049    fn sm_get_messages(
1050        &self,
1051        Parameters(GetMessagesParams {
1052            session_id,
1053            max_tokens,
1054        }): Parameters<GetMessagesParams>,
1055    ) -> Result<String, ErrorData> {
1056        let budget = max_tokens.unwrap_or(4000);
1057        let store = &self.bridge.store;
1058        let result = tokio::task::block_in_place(|| {
1059            Handle::current().block_on(store.get_messages_within_budget(&session_id, budget))
1060        });
1061        match result {
1062            Ok(msgs) => json_to_string(&serde_json::json!({
1063                "ok": true,
1064                "session_id": session_id,
1065                "count": msgs.len(),
1066                "messages": msgs.iter().map(|m| serde_json::json!({
1067                    "id": m.id,
1068                    "role": m.role,
1069                    "content": m.content,
1070                    "token_count": m.token_count,
1071                    "created_at": m.created_at,
1072                })).collect::<Vec<_>>(),
1073            })),
1074            Err(e) => Err(ErrorData::internal_error(
1075                format!("get_messages error: {e}"),
1076                None,
1077            )),
1078        }
1079    }
1080
1081    #[tool(
1082        description = "Hybrid semantic search over stored conversation MESSAGES (not facts). Recall what was discussed in past sessions. Returns ranked messages.",
1083        annotations(read_only_hint = true)
1084    )]
1085    fn sm_search_conversations(
1086        &self,
1087        Parameters(SearchConversationsParams { query, top_k }): Parameters<
1088            SearchConversationsParams,
1089        >,
1090    ) -> Result<String, ErrorData> {
1091        let k = top_k.map(|v| v as usize);
1092        let store = &self.bridge.store;
1093        let result = tokio::task::block_in_place(|| {
1094            Handle::current().block_on(store.search_conversations(&query, k, None))
1095        });
1096        match result {
1097            Ok(results) => json_to_string(&serde_json::json!({
1098                "ok": true,
1099                "count": results.len(),
1100                "results": results.iter().map(|r| serde_json::json!({
1101                    "result_id": r.source.result_id(),
1102                    "content": r.content,
1103                    "score": r.score,
1104                    "cosine_similarity": r.cosine_similarity,
1105                })).collect::<Vec<_>>(),
1106            })),
1107            Err(e) => Err(ErrorData::internal_error(
1108                format!("search_conversations error: {e}"),
1109                None,
1110            )),
1111        }
1112    }
1113
1114    // ── Feature-gated tools ──────────────────────────────────────────
1115    // Note: cfg gates are removed from individual tool methods because
1116    // rmcp's #[tool_router] macro needs all tools visible at expansion
1117    // time. The `full` feature in Cargo.toml already enables the
1118    // semantic-memory sub-features these tools depend on.
1119
1120    #[tool(
1121        description = "Profile a query and get an adaptive routing decision. Determines which retrieval stages (BM25, vector, rerank, graph, decoder, discord) to activate.",
1122        annotations(read_only_hint = true)
1123    )]
1124    fn sm_route_query(
1125        &self,
1126        Parameters(RouteQueryParams { query }): Parameters<RouteQueryParams>,
1127    ) -> Result<String, ErrorData> {
1128        use semantic_memory::routing::RetrievalRouter;
1129
1130        let router = RetrievalRouter {
1131            decoder_enabled: true,
1132            discord_enabled: true,
1133            corpus_density: 0.5,
1134            ..Default::default()
1135        };
1136
1137        let decision = router.route_query(&query);
1138        json_to_string(&serde_json::json!({
1139            "ok": true,
1140            "bm25_coarse": decision.bm25_coarse,
1141            "vector_medium": decision.vector_medium,
1142            "rerank_fine": decision.rerank_fine,
1143            "graph_expansion": decision.graph_expansion,
1144            "decoder": decision.decoder,
1145            "discord": decision.discord,
1146            "no_retrieval": decision.no_retrieval,
1147            "reasoning": decision.reasoning,
1148        }))
1149    }
1150
1151    #[tool(
1152        description = "Adaptive search: profiles query, routes to appropriate stages, applies factor graph belief propagation if decoder is activated. Returns results with stable IDs.",
1153        annotations(read_only_hint = true)
1154    )]
1155    fn sm_search_with_routing(
1156        &self,
1157        Parameters(SearchWithRoutingParams {
1158            query,
1159            top_k,
1160            contradictions,
1161            group_by_community,
1162        }): Parameters<SearchWithRoutingParams>,
1163    ) -> Result<String, ErrorData> {
1164        use semantic_memory::integration::plan_execution;
1165        use semantic_memory::routing::RetrievalRouter;
1166
1167        let k = top_k.map(|v| v as usize).unwrap_or(5);
1168        let allow_superseded = query_allows_superseded(&query);
1169        let search_k = if allow_superseded { k } else { (k * 4).max(20) };
1170        let router = RetrievalRouter {
1171            decoder_enabled: true,
1172            discord_enabled: true,
1173            corpus_density: 0.5,
1174            ..Default::default()
1175        };
1176
1177        let decision = router.route_query(&query);
1178        let contras = contradictions.unwrap_or_default();
1179        let plan = plan_execution(&decision, contras.clone());
1180
1181        let store = &self.bridge.store;
1182        let search_result = tokio::task::block_in_place(|| {
1183            Handle::current().block_on(store.search(&query, Some(search_k), None, None))
1184        });
1185
1186        match search_result {
1187            Ok(results) => {
1188                let superseded_targets = if allow_superseded {
1189                    HashSet::new()
1190                } else {
1191                    load_superseded_targets(store)?
1192                };
1193                let fresh_results: Vec<_> = results
1194                    .iter()
1195                    .filter(|r| !superseded_targets.contains(&r.source.result_id()))
1196                    .collect();
1197                let result_refs: Vec<_> =
1198                    if superseded_targets.is_empty() || fresh_results.is_empty() {
1199                        results.iter().collect()
1200                    } else {
1201                        fresh_results
1202                    };
1203                let superseded_filtered_count = results.len().saturating_sub(result_refs.len());
1204                let json_results: Vec<serde_json::Value> = result_refs
1205                    .iter()
1206                    .take(k)
1207                    .map(|r| {
1208                        serde_json::json!({
1209                            "result_id": r.source.result_id(),
1210                            "content": r.content,
1211                            "score": r.score,
1212                        })
1213                    })
1214                    .collect();
1215
1216                let mut factor_graph_payload = serde_json::json!({
1217                    "enabled": false,
1218                });
1219
1220                let mut decoder_executed = false;
1221                let mut discord_executed = false;
1222                let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
1223
1224                if decision.decoder {
1225                    #[cfg(feature = "full")]
1226                    {
1227                        use semantic_memory::factor_graph::{
1228                            factors_from_edges, FactorGraph, FactorGraphConfig,
1229                        };
1230
1231                        let graph_edges = tokio::task::block_in_place(|| {
1232                            Handle::current().block_on(store.list_all_graph_edges())
1233                        });
1234
1235                        match graph_edges {
1236                            Ok(edges) => {
1237                                let raw_edges: Vec<(
1238                                    String,
1239                                    String,
1240                                    semantic_memory::GraphEdgeType,
1241                                    f64,
1242                                    Option<String>,
1243                                )> = edges
1244                                    .iter()
1245                                    .map(|edge| {
1246                                        let parsed_type = edge
1247                                            .edge_type_parsed
1248                                            .clone()
1249                                            .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1250                                            .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1251                                                relation: "unknown".to_string(),
1252                                            });
1253                                        (
1254                                            edge.source.clone(),
1255                                            edge.target.clone(),
1256                                            parsed_type,
1257                                            edge.weight,
1258                                            edge.metadata.clone(),
1259                                        )
1260                                    })
1261                                    .collect();
1262
1263                                let nodes: Vec<(String, f64)> = result_refs
1264                                    .iter()
1265                                    .map(|r| (r.source.result_id(), r.score))
1266                                    .collect();
1267                                let factors = factors_from_edges(&raw_edges);
1268                                let graph =
1269                                    FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
1270                                let propagated = graph.propagate();
1271                                let top_beliefs = propagated.top_k(k);
1272
1273                                factor_graph_payload = serde_json::json!({
1274                                    "enabled": true,
1275                                    "top_k_beliefs": top_beliefs
1276                                        .into_iter()
1277                                        .map(|(item_id, belief)| serde_json::json!({
1278                                            "item_id": item_id,
1279                                            "belief": belief,
1280                                        }))
1281                                        .collect::<Vec<_>>(),
1282                                    "iterations": propagated.iterations,
1283                                    "converged": propagated.converged,
1284                                    "elapsed_ms": propagated.elapsed_ms,
1285                                    "factor_counts": {
1286                                        "semantic": propagated.factor_counts.semantic,
1287                                        "temporal": propagated.factor_counts.temporal,
1288                                        "causal": propagated.factor_counts.causal,
1289                                        "entity": propagated.factor_counts.entity,
1290                                        "total": propagated.factor_counts.total(),
1291                                    },
1292                                });
1293                                decoder_executed = true;
1294                            }
1295                            Err(e) => {
1296                                factor_graph_payload = serde_json::json!({
1297                                    "enabled": false,
1298                                    "error": format!("factor graph analysis failed: {e}"),
1299                                });
1300                            }
1301                        }
1302                    }
1303
1304                    #[cfg(not(feature = "full"))]
1305                    {
1306                        factor_graph_payload = serde_json::json!({
1307                            "enabled": false,
1308                            "reason": "factor graph analysis requires the `full` feature",
1309                        });
1310                    }
1311
1312                    if !plan.contradictions.is_empty() {
1313                        use semantic_memory::decoder::{compute_correction, detect_syndromes};
1314                        let result_scores: Vec<(String, f64)> = result_refs
1315                            .iter()
1316                            .map(|r| (r.source.result_id(), r.score))
1317                            .collect();
1318                        let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
1319                        let _ = compute_correction(&syndromes, 10.0);
1320                        decoder_executed = true;
1321                    }
1322                }
1323
1324                if plan.use_discord {
1325                    use semantic_memory::discord::DiscordScorer;
1326                    let direct_ids: Vec<String> = result_refs
1327                        .iter()
1328                        .map(|r| r.source.result_id())
1329                        .collect();
1330                    let existing_ids: std::collections::HashSet<String> =
1331                        direct_ids.iter().cloned().collect();
1332                    if let Ok(edges) =
1333                        load_neighborhood_edge_refs(&self.bridge.store, &direct_ids)
1334                    {
1335                        let scorer = DiscordScorer::with_defaults();
1336                        let discord_hits = scorer.score(&direct_ids, &edges);
1337                        for hit in &discord_hits {
1338                            if !existing_ids.contains(&hit.item_id) {
1339                                discord_results_payload.push(serde_json::json!({
1340                                    "result_id": hit.item_id,
1341                                    "discord_score": hit.discord_score,
1342                                    "anchor_ids": hit.anchor_ids,
1343                                    "relationship_types": hit.relationship_types,
1344                                }));
1345                            }
1346                        }
1347                        discord_executed = true;
1348                    }
1349                }
1350
1351                let mut matryoshka_payload = serde_json::json!({
1352                    "enabled": false,
1353                });
1354                if decision.vector_medium {
1355                    #[cfg(feature = "full")]
1356                    {
1357                        use semantic_memory::integration::multi_resolution_route;
1358                        use semantic_memory::matryoshka::MatryoshkaConfig;
1359                        use semantic_memory::routing::QueryProfile;
1360
1361                        let route_profile = QueryProfile::from_query(&query);
1362                        let route_decision =
1363                            multi_resolution_route(&route_profile, &MatryoshkaConfig::default());
1364                        matryoshka_payload = serde_json::json!({
1365                            "enabled": true,
1366                            "candidate_dim": route_decision.candidate_dim,
1367                            "heuristic_recall_estimate": route_decision.estimated_recall,
1368                            "recall_basis": "heuristic_dimensional_model_not_corpus_measured",
1369                            "embedding_dim": route_decision.embedding_dim,
1370                            "reasoning": route_decision.reasoning,
1371                        });
1372                    }
1373
1374                    #[cfg(not(feature = "full"))]
1375                    {
1376                        matryoshka_payload = serde_json::json!({
1377                            "enabled": false,
1378                            "reason": "matryoshka routing requires the `full` feature",
1379                        });
1380                    }
1381                }
1382
1383                // Community grouping (opt-in).
1384                let grouped_results_payload: serde_json::Value =
1385                    if group_by_community == Some(true) {
1386                        let seed_ids: Vec<String> = result_refs
1387                            .iter()
1388                            .take(k)
1389                            .map(|r| r.source.result_id())
1390                            .collect();
1391                        let edges =
1392                            load_neighborhood_edge_pairs(store, &seed_ids).unwrap_or_default();
1393                        if !edges.is_empty() {
1394                            use semantic_memory::community::detect_communities;
1395                            let communities = detect_communities(&edges, 1.0, 42);
1396                            let mut member_to_comm: std::collections::HashMap<String, String> =
1397                                std::collections::HashMap::new();
1398                            for c in &communities {
1399                                for m in &c.members {
1400                                    member_to_comm.insert(m.clone(), c.id.clone());
1401                                }
1402                            }
1403                            let mut groups: std::collections::HashMap<
1404                                String,
1405                                Vec<serde_json::Value>,
1406                            > = std::collections::HashMap::new();
1407                            let mut ungrouped: Vec<serde_json::Value> = Vec::new();
1408                            for r in &json_results {
1409                                if let Some(rid) =
1410                                    r.get("result_id").and_then(|v| v.as_str())
1411                                {
1412                                    match member_to_comm.get(rid).cloned() {
1413                                        Some(cid) => {
1414                                            groups.entry(cid).or_default().push(r.clone())
1415                                        }
1416                                        None => ungrouped.push(r.clone()),
1417                                    }
1418                                }
1419                            }
1420                            let mut map = serde_json::Map::new();
1421                            for (cid, items) in groups {
1422                                map.insert(
1423                                    format!("community_{cid}"),
1424                                    serde_json::json!(items),
1425                                );
1426                            }
1427                            if !ungrouped.is_empty() {
1428                                map.insert(
1429                                    "ungrouped".to_string(),
1430                                    serde_json::json!(ungrouped),
1431                                );
1432                            }
1433                            serde_json::Value::Object(map)
1434                        } else {
1435                            serde_json::Value::Null
1436                        }
1437                    } else {
1438                        serde_json::Value::Null
1439                    };
1440
1441                // Task 7: Auto-call topology when routing returns Class D (SYNTHESIS) and >10 results.
1442                let mut topology_payload = serde_json::json!({ "auto_called": false });
1443                {
1444                    use semantic_memory::routing::{QueryComplexityClass, QueryProfile};
1445                    let route_profile = QueryProfile::from_query(&query);
1446                    if route_profile.complexity_class == QueryComplexityClass::Synthesis
1447                        && result_refs.len() > 10
1448                    {
1449                        #[cfg(feature = "full")]
1450                        {
1451                            use semantic_memory::topology::{compute_betti_numbers, find_voids};
1452                            let edges = load_stored_edge_pairs(store).unwrap_or_default();
1453                            if !edges.is_empty() {
1454                                let mut adjacency: std::collections::HashMap<String, Vec<String>> =
1455                                    std::collections::HashMap::new();
1456                                for (src, tgt) in &edges {
1457                                    adjacency.entry(src.clone()).or_default().push(tgt.clone());
1458                                    adjacency.entry(tgt.clone()).or_default().push(src.clone());
1459                                }
1460                                let betti = compute_betti_numbers(&adjacency);
1461                                let voids = find_voids(&edges);
1462                                topology_payload = serde_json::json!({
1463                                    "auto_called": true,
1464                                    "trigger": "synthesis_class_with_10_plus_results",
1465                                    "betti_numbers": {
1466                                        "betti_0": betti.betti_0,
1467                                        "betti_1": betti.betti_1,
1468                                    },
1469                                    "void_count": voids.len(),
1470                                    "voids": voids.iter().map(|v| serde_json::json!({
1471                                        "description": v.description,
1472                                        "void_type": format!("{:?}", v.void_type),
1473                                        "nearby_items": v.nearby_items,
1474                                        "suggested_connections": v.suggested_connections,
1475                                    })).collect::<Vec<_>>(),
1476                                });
1477                            } else {
1478                                topology_payload = serde_json::json!({
1479                                    "auto_called": true,
1480                                    "trigger": "synthesis_class_with_10_plus_results",
1481                                    "note": "no graph edges in store",
1482                                });
1483                            }
1484                        }
1485                        #[cfg(not(feature = "full"))]
1486                        {
1487                            topology_payload = serde_json::json!({
1488                                "auto_called": true,
1489                                "trigger": "synthesis_class_with_10_plus_results",
1490                                "error": "topology requires the full feature",
1491                            });
1492                        }
1493                    }
1494                }
1495
1496                json_to_string(&serde_json::json!({
1497                    "ok": true,
1498                    "routing_decision": {
1499                        "bm25_coarse": decision.bm25_coarse,
1500                        "vector_medium": decision.vector_medium,
1501                        "rerank_fine": decision.rerank_fine,
1502                        "graph_expansion": decision.graph_expansion,
1503                        "decoder": decision.decoder,
1504                        "discord": decision.discord,
1505                        "no_retrieval": decision.no_retrieval,
1506                        "reasoning": decision.reasoning,
1507                    },
1508                    "results": json_results,
1509                    "count": json_results.len(),
1510                    "superseded_filtered_count": superseded_filtered_count,
1511                    "decoder_planned": plan.use_decoder,
1512                    "decoder_executed": decoder_executed,
1513                    "discord_planned": plan.use_discord,
1514                    "discord_executed": discord_executed,
1515                    "discord_results": discord_results_payload,
1516                    "factor_graph": factor_graph_payload,
1517                    "matryoshka": matryoshka_payload,
1518                    "grouped_results": grouped_results_payload,
1519                    "topology": topology_payload,
1520                }))
1521            }
1522            Err(e) => Err(ErrorData::internal_error(
1523                format!("Search error: {e}"),
1524                None,
1525            )),
1526        }
1527    }
1528
1529    #[tool(
1530        description = "Detect contradictions in search results. Runs syndrome detection, computes corrections, and applies belief propagation to refine confidence scores.",
1531        annotations(read_only_hint = true)
1532    )]
1533    fn sm_decoder_analyze(
1534        &self,
1535        Parameters(DecoderAnalyzeParams {
1536            results,
1537            contradictions,
1538        }): Parameters<DecoderAnalyzeParams>,
1539    ) -> Result<String, ErrorData> {
1540        use semantic_memory::decoder::{
1541            compute_correction, detect_syndromes, pass_messages, ConflictGraph,
1542        };
1543
1544        let contras = contradictions.unwrap_or_default();
1545        let syndromes = detect_syndromes(&results, &contras);
1546        let corrections = compute_correction(&syndromes, 10.0);
1547        let graph = ConflictGraph::from_syndromes(&results, &syndromes);
1548        let mp = pass_messages(&graph, 50, 0.001);
1549
1550        json_to_string(&serde_json::json!({
1551            "ok": true,
1552            "syndromes": syndromes.iter().map(|s| serde_json::json!({
1553                "id": s.id,
1554                "severity": format!("{:?}", s.severity),
1555                "items": s.items,
1556                "description": s.description,
1557                "type": format!("{:?}", s.syndrome_type),
1558            })).collect::<Vec<_>>(),
1559            "syndrome_count": syndromes.len(),
1560            "corrections": corrections.iter().map(|c| serde_json::json!({
1561                "id": c.id,
1562                "confidence": c.confidence,
1563                "cost": c.cost,
1564                "operations": c.operations.len(),
1565            })).collect::<Vec<_>>(),
1566            "correction_count": corrections.len(),
1567            "message_passing": {
1568                "iterations": mp.iterations,
1569                "converged": mp.converged,
1570                "elapsed_ms": mp.elapsed_ms,
1571            },
1572        }))
1573    }
1574
1575    #[tool(
1576        description = "Second-order retrieval: find items related to your search results through the graph, but NOT themselves direct hits. Loads edges from store automatically.",
1577        annotations(read_only_hint = true)
1578    )]
1579    fn sm_discord_search(
1580        &self,
1581        Parameters(DiscordSearchParams { direct_result_ids }): Parameters<DiscordSearchParams>,
1582    ) -> Result<String, ErrorData> {
1583        use semantic_memory::discord::DiscordScorer;
1584
1585        // Use neighborhood loading: only load edges within 2 hops of the
1586        // direct result IDs instead of the entire graph.
1587        let edges = load_neighborhood_edge_refs(&self.bridge.store, &direct_result_ids)?;
1588        let scorer = DiscordScorer::with_defaults();
1589        let results = scorer.score(&direct_result_ids, &edges);
1590
1591        json_to_string(&serde_json::json!({
1592            "ok": true,
1593            "discord_results": results.iter().map(|r| serde_json::json!({
1594                "item_id": r.item_id,
1595                "discord_score": r.discord_score,
1596                "anchor_ids": r.anchor_ids,
1597                "relationship_types": r.relationship_types,
1598            })).collect::<Vec<_>>(),
1599            "count": results.len(),
1600            "edges_loaded": edges.len(),
1601            "edges_scope": "neighborhood",
1602        }))
1603    }
1604
1605    #[tool(
1606        description = "Set provenance (evidence confidence) for an item. Confidence in [0.0, 1.0] with support count. Returns a provenance receipt.",
1607        annotations(idempotent_hint = true)
1608    )]
1609    fn sm_set_provenance(
1610        &self,
1611        Parameters(SetProvenanceParams {
1612            item_id,
1613            confidence,
1614            support_count,
1615        }): Parameters<SetProvenanceParams>,
1616    ) -> Result<String, ErrorData> {
1617        use semantic_memory::provenance::{
1618            ConfidenceSemiring, ConfidenceValue, ProvenanceItemType,
1619        };
1620
1621        // SM-AUD-015: Validate confidence is finite and in [0, 1].
1622        if !confidence.is_finite() || confidence < 0.0 || confidence > 1.0 {
1623            return Err(ErrorData::invalid_params(
1624                format!("confidence must be a finite value in [0.0, 1.0], got {confidence}"),
1625                None,
1626            ));
1627        }
1628
1629        let value = ConfidenceValue::new(confidence, support_count);
1630        let store = &self.bridge.store;
1631
1632        let result = tokio::task::block_in_place(|| {
1633            Handle::current().block_on(store.set_provenance::<ConfidenceSemiring>(
1634                &ProvenanceItemType::Fact,
1635                &item_id,
1636                &value,
1637                &[],
1638                None,
1639            ))
1640        });
1641
1642        match result {
1643            Ok(receipt) => json_to_string(&serde_json::json!({
1644                "ok": true,
1645                "provenance_id": receipt.provenance_id,
1646                "item_id": receipt.item_id,
1647                "semiring_type": receipt.semiring_type,
1648                "recorded_at": receipt.recorded_at,
1649                "message": "Provenance set successfully",
1650            })),
1651            Err(e) => Err(ErrorData::internal_error(
1652                format!("Provenance error: {e}"),
1653                None,
1654            )),
1655        }
1656    }
1657
1658    #[tool(
1659        description = "Run a memory lifecycle pass: analyze items for syndromes, compute corrections, identify subtraction candidates, and check compression needs.",
1660        annotations(read_only_hint = true)
1661    )]
1662    fn sm_run_lifecycle(
1663        &self,
1664        Parameters(RunLifecycleParams { item_ids }): Parameters<RunLifecycleParams>,
1665    ) -> Result<String, ErrorData> {
1666        use semantic_memory::decoder::{compute_correction, detect_syndromes};
1667        use semantic_memory::integration::{
1668            corrections_to_subtraction_candidates, should_trigger_recompression,
1669        };
1670
1671        let results: Vec<(String, f64)> = item_ids.iter().map(|id| (id.clone(), 0.5)).collect();
1672        let syndromes = detect_syndromes(&results, &[]);
1673        let corrections = compute_correction(&syndromes, 10.0);
1674
1675        let sub_candidates = corrections_to_subtraction_candidates(&corrections);
1676
1677        let subtracted_count = sub_candidates.len();
1678        let remaining_count = item_ids.len().saturating_sub(subtracted_count);
1679        let recompression = should_trigger_recompression(subtracted_count, remaining_count, false);
1680
1681        let store = &self.bridge.store;
1682        let graph_edges = tokio::task::block_in_place(|| {
1683            Handle::current().block_on(store.list_all_graph_edges())
1684        });
1685        let stored_edges: Vec<(String, String)> = graph_edges
1686            .as_ref()
1687            .map(|edges| {
1688                edges
1689                    .iter()
1690                    .map(|edge| (edge.source.clone(), edge.target.clone()))
1691                    .collect()
1692            })
1693            .unwrap_or_default();
1694
1695        let mut topology_voids: Vec<serde_json::Value> = Vec::new();
1696        let mut betti = serde_json::json!({
1697            "betti_0": 0usize,
1698            "betti_1": 0usize,
1699        });
1700        let mut topology_error: Option<String> = None;
1701
1702        let mut communities: Vec<serde_json::Value> = Vec::new();
1703        let mut community_contradictions: Vec<serde_json::Value> = Vec::new();
1704        let mut community_error: Option<String> = None;
1705
1706        let mut subgraph_assessment = serde_json::json!({
1707            "subgraphs_identified": 0usize,
1708            "subgraphs_pruned": 0usize,
1709        });
1710        let mut subgraph_error: Option<String> = None;
1711
1712        #[cfg(feature = "full")]
1713        {
1714            use std::collections::HashMap;
1715
1716            if !stored_edges.is_empty() {
1717                let analysis_edges = stored_edges.clone();
1718
1719                let topology_result = (|| -> Result<(), String> {
1720                    use semantic_memory::topology::{compute_betti_numbers, find_voids};
1721
1722                    let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1723                    for (left, right) in &analysis_edges {
1724                        adjacency
1725                            .entry(left.clone())
1726                            .or_default()
1727                            .push(right.clone());
1728                        adjacency
1729                            .entry(right.clone())
1730                            .or_default()
1731                            .push(left.clone());
1732                    }
1733
1734                    let betti_numbers = compute_betti_numbers(&adjacency);
1735                    betti = serde_json::json!({
1736                        "betti_0": betti_numbers.betti_0,
1737                        "betti_1": betti_numbers.betti_1,
1738                    });
1739
1740                    topology_voids = find_voids(&analysis_edges)
1741                        .into_iter()
1742                        .map(|v| {
1743                            serde_json::json!({
1744                                "description": v.description,
1745                                "void_type": format!("{:?}", v.void_type),
1746                                "nearby_items": v.nearby_items,
1747                                "suggested_connections": v.suggested_connections,
1748                            })
1749                        })
1750                        .collect();
1751
1752                    Ok(())
1753                })();
1754
1755                if let Err(e) = topology_result {
1756                    topology_error = Some(e);
1757                }
1758
1759                let community_result = (|| -> Result<(), String> {
1760                    use semantic_memory::community::{
1761                        community_contradiction_scan, detect_communities,
1762                    };
1763
1764                    let detected = detect_communities(&analysis_edges, 1.0, 42);
1765                    communities = detected
1766                        .iter()
1767                        .map(|c| {
1768                            serde_json::json!({
1769                                "id": c.id,
1770                                "members": c.members,
1771                                "level": c.level,
1772                                "parent": c.parent,
1773                                "member_count": c.members.len(),
1774                            })
1775                        })
1776                        .collect();
1777
1778                    community_contradictions = community_contradiction_scan(&detected, &[])
1779                        .into_iter()
1780                        .map(|cc| {
1781                            serde_json::json!({
1782                                "community_id": cc.community_id,
1783                                "item_a": cc.item_a,
1784                                "item_b": cc.item_b,
1785                                "description": cc.description,
1786                            })
1787                        })
1788                        .collect();
1789
1790                    Ok(())
1791                })();
1792
1793                if let Err(e) = community_result {
1794                    community_error = Some(e);
1795                }
1796
1797                let subgraph_result = (|| -> Result<(), String> {
1798                    use semantic_memory::integration::autonomous_subgraph_maintenance;
1799                    use semantic_memory::subgraph_pruning::AccessLog;
1800                    use std::collections::HashSet;
1801
1802                    let mut access_items: HashSet<String> = HashSet::new();
1803                    for (left, right) in &analysis_edges {
1804                        access_items.insert(left.clone());
1805                        access_items.insert(right.clone());
1806                    }
1807
1808                    let access_logs = access_items
1809                        .into_iter()
1810                        .map(|item| AccessLog {
1811                            item_id: item,
1812                            access_count: 1,
1813                            last_accessed: "1970-01-01T00:00:00Z".to_string(),
1814                        })
1815                        .collect::<Vec<_>>();
1816
1817                    let report =
1818                        autonomous_subgraph_maintenance(&analysis_edges, &access_logs, &[], 0);
1819                    subgraph_assessment = serde_json::json!({
1820                        "subgraphs_identified": report.subgraphs_identified,
1821                        "subgraphs_pruned": report.subgraphs_pruned,
1822                        "summary": report.summary,
1823                    });
1824                    Ok(())
1825                })();
1826
1827                if let Err(e) = subgraph_result {
1828                    subgraph_error = Some(e);
1829                }
1830            }
1831        }
1832
1833        #[cfg(not(feature = "full"))]
1834        {
1835            if !stored_edges.is_empty() {
1836                topology_error = Some(
1837                    "topology/community/subgraph phases require the `full` feature".to_string(),
1838                );
1839                community_error = Some(
1840                    "topology/community/subgraph phases require the `full` feature".to_string(),
1841                );
1842                subgraph_error = Some(
1843                    "topology/community/subgraph phases require the `full` feature".to_string(),
1844                );
1845            }
1846        }
1847
1848        #[cfg(feature = "full")]
1849        let (f32_count, compressed_count) =
1850            item_ids
1851                .iter()
1852                .fold((0usize, 0usize), |(f32_count, compressed_count), _| {
1853                    use semantic_memory::compression_governor::{
1854                        decide_quantization, QuantizationLevel,
1855                    };
1856
1857                    match decide_quantization(0.5) {
1858                        QuantizationLevel::F32 => (f32_count + 1, compressed_count),
1859                        _ => (f32_count, compressed_count + 1),
1860                    }
1861                });
1862        #[cfg(not(feature = "full"))]
1863        let (f32_count, compressed_count) = (0usize, 0usize);
1864
1865        json_to_string(&serde_json::json!({
1866            "ok": true,
1867            "items_analyzed": item_ids.len(),
1868            "syndromes_detected": syndromes.len(),
1869            "corrections_computed": corrections.len(),
1870            "subtraction_candidates": sub_candidates.iter().map(|c| serde_json::json!({
1871                "item_id": c.item_id,
1872                "structuring_score": c.structuring_score,
1873                "operation_type": c.operation_type,
1874                "reason": c.reason,
1875            })).collect::<Vec<_>>(),
1876            "recompression_triggered": recompression.triggered,
1877            "recompression_reason": recompression.reason,
1878            "topology": {
1879                "enabled": !stored_edges.is_empty(),
1880                "voids": topology_voids,
1881                "void_count": topology_voids.len(),
1882                "betti_numbers": betti,
1883                "error": topology_error,
1884            },
1885            "community_detection": {
1886                "enabled": !stored_edges.is_empty(),
1887                "communities": communities,
1888                "community_count": communities.len(),
1889                "contradictions": community_contradictions,
1890                "contradiction_count": community_contradictions.len(),
1891                "error": community_error,
1892            },
1893            "subgraph_pruning_assessment": {
1894                "enabled": !stored_edges.is_empty(),
1895                "subgraph_count": subgraph_assessment["subgraphs_identified"].as_u64().unwrap_or(0),
1896                "pruned_count": subgraph_assessment["subgraphs_pruned"].as_u64().unwrap_or(0),
1897                "summary": subgraph_assessment["summary"].as_str().unwrap_or(""),
1898                "error": subgraph_error,
1899            },
1900            "turbo_quantization_assessment": {
1901                "items_assessed": item_ids.len(),
1902                "would_retain_f32": f32_count,
1903                "would_compress": compressed_count,
1904            },
1905            "summary": format!(
1906                "Analyzed {} items: {} syndromes, {} corrections, {} subtraction candidates, recompression: {}",
1907                item_ids.len(), syndromes.len(), corrections.len(), sub_candidates.len(),
1908                if recompression.triggered { "needed" } else { "not needed" }
1909            ),
1910        }))
1911    }
1912
1913    // ── First-class graph edge tools ───────────────────────────────
1914
1915    #[tool(
1916        description = "Add a durable, typed graph edge between two nodes. Edge types: semantic, temporal, causal, entity. Idempotent — same edge returns existing ID.",
1917        annotations(idempotent_hint = true)
1918    )]
1919    fn sm_add_graph_edge(
1920        &self,
1921        Parameters(params): Parameters<AddGraphEdgeParams>,
1922    ) -> Result<String, ErrorData> {
1923        use semantic_memory::GraphEdgeType;
1924
1925        // SM-AUD-015: Validate numeric params are finite and in range.
1926        if let Some(cs) = params.cosine_similarity {
1927            if !cs.is_finite() || cs < 0.0 || cs > 1.0 {
1928                return Err(ErrorData::invalid_params(
1929                    format!("cosine_similarity must be finite and in [0.0, 1.0], got {cs}"),
1930                    None,
1931                ));
1932            }
1933        }
1934        if let Some(conf) = params.confidence {
1935            if !conf.is_finite() || conf < 0.0 || conf > 1.0 {
1936                return Err(ErrorData::invalid_params(
1937                    format!("confidence must be finite and in [0.0, 1.0], got {conf}"),
1938                    None,
1939                ));
1940            }
1941        }
1942
1943        let edge_type = match params.edge_type {
1944            EdgeType::Semantic => GraphEdgeType::Semantic {
1945                cosine_similarity: params.cosine_similarity.unwrap_or(0.5),
1946            },
1947            EdgeType::Temporal => GraphEdgeType::Temporal {
1948                delta_secs: params.delta_secs.unwrap_or(0),
1949            },
1950            EdgeType::Causal => GraphEdgeType::Causal {
1951                confidence: params.confidence.unwrap_or(0.5),
1952                evidence_ids: params.evidence_ids.unwrap_or_default(),
1953            },
1954            EdgeType::Entity => GraphEdgeType::Entity {
1955                relation: params.relation.unwrap_or_else(|| "related".to_string()),
1956            },
1957        };
1958
1959        // MCP-004: Reject malformed metadata JSON instead of silently dropping it.
1960        let metadata = match params.metadata.as_deref() {
1961            None => None,
1962            Some(s) => match serde_json::from_str::<serde_json::Value>(s) {
1963                Ok(v) => Some(v),
1964                Err(e) => {
1965                    return Err(ErrorData::invalid_params(
1966                        format!("metadata is not valid JSON: {e}"),
1967                        None,
1968                    ))
1969                }
1970            },
1971        };
1972
1973        let store = &self.bridge.store;
1974        let result = tokio::task::block_in_place(|| {
1975            Handle::current().block_on(store.add_graph_edge(
1976                &params.source,
1977                &params.target,
1978                edge_type,
1979                params.weight,
1980                metadata,
1981            ))
1982        });
1983
1984        match result {
1985            Ok(edge) => json_to_string(&serde_json::json!({
1986                "ok": true,
1987                "id": edge.id,
1988                "source": edge.source,
1989                "target": edge.target,
1990                "edge_type": edge.edge_type,
1991                "weight": edge.weight,
1992                "content_digest": edge.content_digest,
1993                "recorded_at": edge.recorded_at,
1994                "message": "Graph edge added successfully",
1995            })),
1996            Err(e) => Err(ErrorData::internal_error(
1997                format!("Error adding graph edge: {e}"),
1998                None,
1999            )),
2000        }
2001    }
2002
2003    #[tool(
2004        description = "List graph edges for a specific node (as source or target), or all edges if no node_id. Returns non-invalidated edges only.",
2005        annotations(read_only_hint = true)
2006    )]
2007    fn sm_list_graph_edges(
2008        &self,
2009        Parameters(ListGraphEdgesParams { node_id }): Parameters<ListGraphEdgesParams>,
2010    ) -> Result<String, ErrorData> {
2011        let store = &self.bridge.store;
2012        let result = match node_id {
2013            Some(id) => tokio::task::block_in_place(|| {
2014                Handle::current().block_on(store.list_graph_edges_for_node(&id))
2015            }),
2016            None => tokio::task::block_in_place(|| {
2017                Handle::current().block_on(store.list_all_graph_edges())
2018            }),
2019        };
2020
2021        match result {
2022            Ok(edges) => json_to_string(&serde_json::json!({
2023                "ok": true,
2024                "edges": edges.iter().map(|e| serde_json::json!({
2025                    "id": e.id,
2026                    "source": e.source,
2027                    "target": e.target,
2028                    "edge_type": e.edge_type,
2029                    "weight": e.weight,
2030                    "metadata": e.metadata,
2031                    "recorded_at": e.recorded_at,
2032                })).collect::<Vec<_>>(),
2033                "count": edges.len(),
2034            })),
2035            Err(e) => Err(ErrorData::internal_error(
2036                format!("Error listing graph edges: {e}"),
2037                None,
2038            )),
2039        }
2040    }
2041
2042    #[tool(
2043        description = "Invalidate a stored graph edge by ID. Append-only — edge is never deleted, only marked invalidated with a reason.",
2044        annotations(idempotent_hint = true)
2045    )]
2046    fn sm_invalidate_graph_edge(
2047        &self,
2048        Parameters(InvalidateGraphEdgeParams { edge_id, reason }): Parameters<
2049            InvalidateGraphEdgeParams,
2050        >,
2051    ) -> Result<String, ErrorData> {
2052        let store = &self.bridge.store;
2053        let result = tokio::task::block_in_place(|| {
2054            Handle::current().block_on(store.invalidate_graph_edge(&edge_id, &reason))
2055        });
2056
2057        match result {
2058            Ok(()) => json_to_string(&serde_json::json!({
2059                "ok": true,
2060                "edge_id": edge_id,
2061                "message": "Edge invalidated successfully",
2062            })),
2063            Err(e) => Err(ErrorData::internal_error(
2064                format!("Error invalidating edge: {e}"),
2065                None,
2066            )),
2067        }
2068    }
2069
2070    // ── Factor graph, topology, and community tools ─────────────────
2071
2072    #[tool(
2073        description = "Run factor graph belief propagation on stored graph edges. Models all 4 edge types as factors. Returns unified confidence scores after convergence.",
2074        annotations(read_only_hint = true)
2075    )]
2076    fn sm_factor_graph(
2077        &self,
2078        Parameters(params): Parameters<FactorGraphParams>,
2079    ) -> Result<String, ErrorData> {
2080        use semantic_memory::factor_graph::{factors_from_edges, FactorGraph, FactorGraphConfig};
2081
2082        let defaults = FactorGraphConfig::default();
2083        let config = FactorGraphConfig {
2084            semantic_weight: params.semantic_weight.unwrap_or(defaults.semantic_weight),
2085            temporal_weight: params.temporal_weight.unwrap_or(defaults.temporal_weight),
2086            causal_weight: params.causal_weight.unwrap_or(defaults.causal_weight),
2087            entity_weight: params.entity_weight.unwrap_or(defaults.entity_weight),
2088            self_influence: params.self_influence.unwrap_or(defaults.self_influence),
2089            max_iterations: params
2090                .max_iterations
2091                .map(|v| v as usize)
2092                .unwrap_or(defaults.max_iterations),
2093            convergence_threshold: params
2094                .convergence_threshold
2095                .unwrap_or(defaults.convergence_threshold),
2096        };
2097
2098        // Use neighborhood loading: only load edges within 2 hops of the
2099        // node seeds instead of the entire graph.
2100        let seed_ids: Vec<String> = params.nodes.iter().map(|n| n.item_id.clone()).collect();
2101        let raw_edges = load_neighborhood_factor_edges(&self.bridge.store, &seed_ids)?;
2102        let factors = factors_from_edges(&raw_edges);
2103
2104        let nodes: Vec<(String, f64)> = params
2105            .nodes
2106            .iter()
2107            .map(|n| (n.item_id.clone(), n.initial_belief))
2108            .collect();
2109
2110        let graph = FactorGraph::new(&nodes, factors, config);
2111        let result = graph.propagate();
2112
2113        json_to_string(&serde_json::json!({
2114            "ok": true,
2115            "node_beliefs": result.node_beliefs,
2116            "iterations": result.iterations,
2117            "converged": result.converged,
2118            "elapsed_ms": result.elapsed_ms,
2119            "edges_loaded": raw_edges.len(),
2120            "edges_scope": "neighborhood",
2121            "factor_counts": {
2122                "semantic": result.factor_counts.semantic,
2123                "temporal": result.factor_counts.temporal,
2124                "causal": result.factor_counts.causal,
2125                "entity": result.factor_counts.entity,
2126                "total": result.factor_counts.total(),
2127            },
2128            "config": {
2129                "semantic_weight": result.config.semantic_weight,
2130                "temporal_weight": result.config.temporal_weight,
2131                "causal_weight": result.config.causal_weight,
2132                "entity_weight": result.config.entity_weight,
2133                "self_influence": result.config.self_influence,
2134                "max_iterations": result.config.max_iterations,
2135                "convergence_threshold": result.config.convergence_threshold,
2136            },
2137        }))
2138    }
2139
2140    #[tool(
2141        description = "Find topological voids in the knowledge graph. Computes Betti numbers (components and cycles) and detects structural gaps. Loads edges from store.",
2142        annotations(read_only_hint = true)
2143    )]
2144    fn sm_topology(
2145        &self,
2146        Parameters(_params): Parameters<TopologyParams>,
2147    ) -> Result<String, ErrorData> {
2148        use semantic_memory::topology::{compute_betti_numbers, find_voids, gap_report};
2149
2150        // MCP-001: Load edges from the store, not from caller-supplied params.
2151        let edges = load_stored_edge_pairs(&self.bridge.store)?;
2152
2153        let mut adjacency: std::collections::HashMap<String, Vec<String>> =
2154            std::collections::HashMap::new();
2155        for (src, tgt) in &edges {
2156            adjacency.entry(src.clone()).or_default().push(tgt.clone());
2157            adjacency.entry(tgt.clone()).or_default().push(src.clone());
2158        }
2159
2160        let betti = compute_betti_numbers(&adjacency);
2161        let voids = find_voids(&edges);
2162        let report = gap_report(&voids);
2163
2164        json_to_string(&serde_json::json!({
2165            "ok": true,
2166            "betti_numbers": {
2167                "betti_0": betti.betti_0,
2168                "betti_1": betti.betti_1,
2169            },
2170            "voids": voids.iter().map(|v| serde_json::json!({
2171                "description": v.description,
2172                "nearby_items": v.nearby_items,
2173                "suggested_connections": v.suggested_connections,
2174                "void_type": format!("{:?}", v.void_type),
2175            })).collect::<Vec<_>>(),
2176            "void_count": voids.len(),
2177            "edges_loaded_from_store": edges.len(),
2178            "report": report,
2179        }))
2180    }
2181
2182    #[tool(
2183        description = "Detect communities in the knowledge graph (Leiden-inspired). Returns community assignments, optional contradiction scans, and compression recommendations.",
2184        annotations(read_only_hint = true)
2185    )]
2186    fn sm_community(
2187        &self,
2188        Parameters(params): Parameters<CommunityParams>,
2189    ) -> Result<String, ErrorData> {
2190        use semantic_memory::community::{
2191            community_aware_compression, community_contradiction_scan, detect_communities,
2192        };
2193
2194        // MCP-001: Load edges from the store, not from caller-supplied params.
2195        let edges = load_stored_edge_pairs(&self.bridge.store)?;
2196
2197        let resolution = params.resolution.unwrap_or(1.0);
2198        let seed = params.seed.unwrap_or(42);
2199
2200        let communities = detect_communities(&edges, resolution, seed);
2201
2202        let contradictions = params.contradictions.unwrap_or_default();
2203        let community_contras = community_contradiction_scan(&communities, &contradictions);
2204
2205        let importance_scores = params.importance_scores.unwrap_or_default();
2206        let compression = community_aware_compression(&communities, &importance_scores);
2207
2208        let summarize = params.summarize.unwrap_or(false);
2209        let store = &self.bridge.store;
2210        let communities_json: Vec<serde_json::Value> = communities
2211            .iter()
2212            .map(|c| {
2213                let summary: Option<String> = if summarize && !c.members.is_empty() {
2214                    let member_texts: Vec<String> = c
2215                        .members
2216                        .iter()
2217                        .filter_map(|mid| {
2218                            let bare = mid.strip_prefix("fact:").unwrap_or(mid);
2219                            tokio::task::block_in_place(|| {
2220                                Handle::current().block_on(store.get_fact(bare))
2221                            })
2222                            .ok()
2223                            .flatten()
2224                            .map(|f| f.content)
2225                        })
2226                        .collect();
2227                    if !member_texts.is_empty() {
2228                        let combined = member_texts.join("\n---\n");
2229                        let prompt = format!(
2230                            "Summarize these related facts in 1-2 sentences:\n{combined}\nSummary:"
2231                        );
2232                        let body = serde_json::json!({
2233                            "model": "granite4.1:3b",
2234                            "prompt": prompt,
2235                            "stream": false,
2236                            "options": {"temperature": 0, "num_predict": 100}
2237                        });
2238                        reqwest::blocking::Client::new()
2239                            .post("http://127.0.0.1:11434/api/generate")
2240                            .json(&body)
2241                            .send()
2242                            .ok()
2243                            .and_then(|resp| resp.json::<serde_json::Value>().ok())
2244                            .and_then(|v| {
2245                                v.get("response")
2246                                    .and_then(|r| r.as_str())
2247                                    .map(|s| s.trim().to_string())
2248                            })
2249                    } else {
2250                        None
2251                    }
2252                } else {
2253                    None
2254                };
2255                serde_json::json!({
2256                    "id": c.id,
2257                    "members": c.members,
2258                    "level": c.level,
2259                    "parent": c.parent,
2260                    "member_count": c.members.len(),
2261                    "summary": summary,
2262                })
2263            })
2264            .collect();
2265
2266        json_to_string(&serde_json::json!({
2267            "ok": true,
2268            "communities": communities_json,
2269            "community_count": communities.len(),
2270            "contradictions": community_contras.iter().map(|cc| serde_json::json!({
2271                "community_id": cc.community_id,
2272                "item_a": cc.item_a,
2273                "item_b": cc.item_b,
2274                "description": cc.description,
2275            })).collect::<Vec<_>>(),
2276            "contradiction_count": community_contras.len(),
2277            "compression_recommendations": compression.iter().map(|cr| serde_json::json!({
2278                "community_id": cr.community_id,
2279                "quantization_level": cr.quantization_level,
2280                "reason": cr.reason,
2281            })).collect::<Vec<_>>(),
2282            "compression_count": compression.len(),
2283            "edges_loaded_from_store": edges.len(),
2284        }))
2285    }
2286
2287    // ── Delete / forget tools (admin-ops) ────────────────────────────
2288    // Hard removal. Prefer sm_supersede_fact when there is a corrected
2289    // replacement (it keeps history and search filters the old one); use
2290    // delete only for true noise/errors that should vanish entirely.
2291
2292    #[tool(
2293        description = "Permanently delete a single fact by id. HARD delete — removes fact and its FTS/vector entries. Irreversible. Prefer sm_supersede_fact for corrections.",
2294        annotations(destructive_hint = true)
2295    )]
2296    fn sm_delete_fact(
2297        &self,
2298        Parameters(DeleteFactParams { fact_id }): Parameters<DeleteFactParams>,
2299    ) -> Result<String, ErrorData> {
2300        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2301        let store = &self.bridge.store;
2302        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_fact(&bare)));
2303        match result {
2304            Ok(()) => json_to_string(&serde_json::json!({
2305                "ok": true,
2306                "deleted": true,
2307                "fact_id": format!("fact:{bare}"),
2308                "message": "Fact permanently deleted",
2309            })),
2310            Err(e) => Err(ErrorData::internal_error(format!("delete_fact error: {e}"), None)),
2311        }
2312    }
2313
2314    #[tool(
2315        description = "Permanently delete ALL memory in a namespace — facts, documents, chunks, sessions/messages. HARD delete, irreversible. Returns per-surface deletion count.",
2316        annotations(destructive_hint = true)
2317    )]
2318    fn sm_delete_namespace(
2319        &self,
2320        Parameters(DeleteNamespaceParams { namespace }): Parameters<DeleteNamespaceParams>,
2321    ) -> Result<String, ErrorData> {
2322        let store = &self.bridge.store;
2323        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.delete_namespace(&namespace)));
2324        match result {
2325            Ok(r) => json_to_string(&serde_json::json!({
2326                "ok": true,
2327                "namespace": namespace,
2328                "deleted": {
2329                    "facts": r.facts,
2330                    "documents": r.documents,
2331                    "chunks": r.chunks,
2332                    "messages": r.messages,
2333                    "sessions": r.sessions,
2334                    "episodes": r.episodes,
2335                    "projection_rows": r.projection_rows,
2336                },
2337                "message": "Namespace permanently deleted",
2338            })),
2339            Err(e) => Err(ErrorData::internal_error(format!("delete_namespace error: {e}"), None)),
2340        }
2341    }
2342
2343    #[tool(
2344        description = "Update a fact's content in-place. Re-embeds the fact and updates FTS index. Use this to correct outdated facts without deleting and re-adding.",
2345        annotations(idempotent_hint = true)
2346    )]
2347    fn sm_update_fact(
2348        &self,
2349        Parameters(UpdateFactParams { fact_id, content }): Parameters<UpdateFactParams>,
2350    ) -> Result<String, ErrorData> {
2351        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2352        let store = &self.bridge.store;
2353        let result = tokio::task::block_in_place(|| Handle::current().block_on(store.update_fact(&bare, &content)));
2354        match result {
2355            Ok(()) => json_to_string(&serde_json::json!({
2356                "ok": true,
2357                "fact_id": format!("fact:{bare}"),
2358                "message": "Fact content updated and re-embedded",
2359            })),
2360            Err(e) => Err(ErrorData::internal_error(format!("update_fact error: {e}"), None)),
2361        }
2362    }
2363
2364    #[tool(
2365        description = "Consolidate two near-duplicate facts into one. Merges their content, updates the kept fact, and supersedes the other with a 'consolidated with' edge. Use this to clean up duplicate knowledge."
2366    )]
2367    fn sm_consolidate_facts(
2368        &self,
2369        Parameters(ConsolidateFactsParams { keep_id, supersede_id, merged_content }): Parameters<ConsolidateFactsParams>,
2370    ) -> Result<String, ErrorData> {
2371        let keep_bare = keep_id.strip_prefix("fact:").unwrap_or(&keep_id).to_string();
2372        let sup_bare = supersede_id.strip_prefix("fact:").unwrap_or(&supersede_id).to_string();
2373        let store = &self.bridge.store;
2374
2375        // Get both facts to determine namespace and merge content
2376        let keep_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&keep_bare)));
2377        let sup_fact = tokio::task::block_in_place(|| Handle::current().block_on(store.get_fact(&sup_bare)));
2378
2379        let (namespace, final_content) = match (keep_fact, sup_fact) {
2380            (Ok(Some(k)), Ok(Some(s))) => {
2381                let ns = k.namespace.clone();
2382                let content = merged_content.unwrap_or_else(|| {
2383                    if k.content.len() >= s.content.len() {
2384                        if !k.content.contains(&s.content) {
2385                            format!("{}\n\nAdditional: {}", k.content, s.content)
2386                        } else {
2387                            k.content.clone()
2388                        }
2389                    } else if !s.content.contains(&k.content) {
2390                        format!("{}\n\nAdditional: {}", s.content, k.content)
2391                    } else {
2392                        s.content.clone()
2393                    }
2394                });
2395                (ns, content)
2396            }
2397            (Ok(Some(k)), _) => (
2398                k.namespace.clone(),
2399                merged_content.unwrap_or(k.content.clone()),
2400            ),
2401            (Err(_), _) | (Ok(None), _) => {
2402                return Err(ErrorData::internal_error(
2403                    format!("keep fact not found"),
2404                    None,
2405                ));
2406            }
2407        };
2408
2409        // Update the kept fact with merged content
2410        let update_result = tokio::task::block_in_place(|| {
2411            Handle::current().block_on(store.update_fact(&keep_bare, &final_content))
2412        });
2413        if let Err(e) = update_result {
2414            return Err(ErrorData::internal_error(format!("update keep fact error: {e}"), None));
2415        }
2416
2417        // Supersede the other fact: add a new fact with merged content and link with "supersedes" edge
2418        use semantic_memory::GraphEdgeType;
2419        let new_id = tokio::task::block_in_place(|| {
2420            Handle::current().block_on(store.add_fact(&namespace, &final_content, None, None))
2421        });
2422        match new_id {
2423            Ok(nid) => {
2424                let new_node = format!("fact:{nid}");
2425                let old_node = format!("fact:{sup_bare}");
2426                let metadata = serde_json::json!({
2427                    "reason": "consolidated duplicate",
2428                    "consolidated_with": format!("fact:{}", keep_bare),
2429                });
2430                let _edge = tokio::task::block_in_place(|| {
2431                    Handle::current().block_on(store.add_graph_edge(
2432                        &new_node,
2433                        &old_node,
2434                        GraphEdgeType::Entity {
2435                            relation: "supersedes".to_string(),
2436                        },
2437                        1.0,
2438                        Some(metadata),
2439                    ))
2440                });
2441                json_to_string(&serde_json::json!({
2442                    "ok": true,
2443                    "kept_fact_id": format!("fact:{}", keep_bare),
2444                    "superseded_fact_id": format!("fact:{}", sup_bare),
2445                    "new_fact_id": format!("fact:{}", nid),
2446                    "message": "Facts consolidated: kept fact updated, duplicate superseded",
2447                }))
2448            }
2449            Err(e) => Err(ErrorData::internal_error(
2450                format!("supersede error: {e}"),
2451                None,
2452            )),
2453        }
2454    }
2455
2456    // ── RL routing feedback ────────────────────────────────────────────
2457
2458    #[tool(
2459        description = "Record routing outcome feedback for RL-trained retrieval routing. Stores the outcome (good/bad/neutral) and updates the tabular routing policy Q-table. Use after sm_search_with_routing to provide feedback on routing quality.",
2460        annotations(read_only_hint = true)
2461    )]
2462    fn sm_record_outcome(
2463        &self,
2464        Parameters(RecordOutcomeParams { query, outcome }): Parameters<RecordOutcomeParams>,
2465    ) -> Result<String, ErrorData> {
2466        use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome, RoutingPolicy};
2467        use semantic_memory::routing::{QueryProfile, RetrievalRouter};
2468
2469        let outcome_enum = match outcome.to_lowercase().as_str() {
2470            "good" => RoutingOutcome::Good,
2471            "bad" => RoutingOutcome::Bad,
2472            "neutral" => RoutingOutcome::Neutral,
2473            _ => {
2474                return Err(ErrorData::invalid_params(
2475                    format!("outcome must be 'good', 'bad', or 'neutral', got '{outcome}'"),
2476                    None,
2477                ));
2478            }
2479        };
2480
2481        let profile = QueryProfile::from_query(&query);
2482        let router = RetrievalRouter::default();
2483        let decision = router.route(&profile);
2484
2485        let mut policy = RoutingPolicy::default();
2486        record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
2487
2488        json_to_string(&serde_json::json!({
2489            "ok": true,
2490            "query": query,
2491            "outcome": outcome,
2492            "routing_decision": {
2493                "bm25_coarse": decision.bm25_coarse,
2494                "vector_medium": decision.vector_medium,
2495                "rerank_fine": decision.rerank_fine,
2496                "graph_expansion": decision.graph_expansion,
2497                "decoder": decision.decoder,
2498                "discord": decision.discord,
2499                "no_retrieval": decision.no_retrieval,
2500                "reasoning": decision.reasoning,
2501            },
2502            "policy_state": {
2503                "trained_examples": policy.trained_examples,
2504                "baseline": policy.baseline,
2505                "weights": policy.weights,
2506            },
2507            "message": "Routing outcome recorded and policy updated",
2508        }))
2509    }
2510
2511    // ─── Claim-ledger integration ──────────────────────────────────────
2512
2513    #[cfg(feature = "claim-integration")]
2514    #[tool(
2515        description = "Create a typed Claim from a semantic-memory fact. The claim gets a source-spanned provenance record from the fact's metadata. Returns the claim ID.",
2516        annotations(read_only_hint = false, idempotent_hint = true)
2517    )]
2518    fn sm_create_claim(
2519        &self,
2520        Parameters(CreateClaimParams { fact_id, source_span }): Parameters<CreateClaimParams>,
2521    ) -> Result<String, ErrorData> {
2522        use claim_ledger::Claim;
2523        let bare = fact_id.strip_prefix("fact:").unwrap_or(&fact_id).to_string();
2524        let store = &self.bridge.store;
2525
2526        // Get the fact content
2527        let fact = tokio::task::block_in_place(|| {
2528            Handle::current().block_on(store.get_fact(&bare))
2529        });
2530        let fact = match fact {
2531            Ok(Some(f)) => f,
2532            _ => return Err(ErrorData::internal_error(format!("fact not found: {fact_id}"), None)),
2533        };
2534
2535        // Create a claim from the fact
2536        let source_id = format!("semantic-memory:fact:{bare}");
2537        let span_id = source_span.unwrap_or_else(|| "full".to_string());
2538        let claim = Claim::new(&source_id, &span_id, &fact.content, "fact");
2539
2540        let claim_id = claim.claim_id.clone();
2541        let normalized = &claim.normalized_claim;
2542
2543        json_to_string(&serde_json::json!({
2544            "ok": true,
2545            "claim_id": claim_id,
2546            "source_id": source_id,
2547            "span_id": span_id,
2548            "claim_text": fact.content,
2549            "normalized_claim": normalized,
2550            "claim_type": "fact",
2551            "message": "Claim created from semantic-memory fact with source-spanned provenance",
2552        }))
2553    }
2554
2555    #[cfg(feature = "claim-integration")]
2556    #[tool(
2557        description = "Add evidence to a claim. Creates an EvidenceBundle linking the evidence text to the claim. Returns the evidence bundle ID.",
2558        annotations(read_only_hint = false)
2559    )]
2560    fn sm_add_evidence(
2561        &self,
2562        Parameters(AddEvidenceParams {
2563            claim_id,
2564            evidence_text,
2565            source_type,
2566        }): Parameters<AddEvidenceParams>,
2567    ) -> Result<String, ErrorData> {
2568        use claim_ledger::{EvidenceBundle, EvidenceLink, EvidenceRelation};
2569        let mut bundle = EvidenceBundle::new(&claim_id);
2570        let link = EvidenceLink {
2571            relation: EvidenceRelation::Supports,
2572            source_id: source_type.unwrap_or_else(|| "semantic-memory".to_string()),
2573            span_id: "full".to_string(),
2574            quote: evidence_text.clone(),
2575            digest: claim_ledger::ids::sha256_text(&evidence_text),
2576            support_role: "supporting".to_string(),
2577        };
2578        bundle.evidence_links.push(link);
2579
2580        json_to_string(&serde_json::json!({
2581            "ok": true,
2582            "evidence_bundle_id": bundle.evidence_bundle_id,
2583            "claim_id": claim_id,
2584            "evidence_count": bundle.evidence_links.len(),
2585            "message": "Evidence added to claim",
2586        }))
2587    }
2588
2589    #[cfg(feature = "claim-integration")]
2590    #[tool(
2591        description = "Judge the support state of a claim. Creates a SupportJudgment (supported, unsupported, contested, or heuristic_only) with optional rationale.",
2592        annotations(read_only_hint = false)
2593    )]
2594    fn sm_judge_support(
2595        &self,
2596        Parameters(JudgeSupportParams { claim_id, judgment, rationale }): Parameters<JudgeSupportParams>,
2597    ) -> Result<String, ErrorData> {
2598        use claim_ledger::{SupportJudgment, SupportState};
2599        let state = match judgment.to_lowercase().as_str() {
2600            "supported" => SupportState::Supported,
2601            "partially_supported" | "partial" => SupportState::PartiallySupported,
2602            "unsupported" => SupportState::Unsupported,
2603            "contradicted" | "contested" => SupportState::Contradicted,
2604            "heuristic_only" | "heuristic" => SupportState::HeuristicOnly,
2605            _ => return Err(ErrorData::invalid_params(
2606                format!("Invalid judgment '{judgment}'. Must be: supported, partially_supported, unsupported, contradicted, or heuristic_only"),
2607                None,
2608            )),
2609        };
2610        let j = SupportJudgment {
2611            support_judgment_id: claim_ledger::ids::ulid(),
2612            claim_id: claim_id.clone(),
2613            evidence_bundle_ref: claim_ledger::ids::evidence_bundle_id(&claim_id),
2614            support_state: state,
2615            method: "agent_judgment".to_string(),
2616            rationale: rationale.unwrap_or_default(),
2617            contradiction_refs: Vec::new(),
2618            proof_debt: Vec::new(),
2619            created_recorded_time: chrono::Utc::now(),
2620        };
2621
2622        json_to_string(&serde_json::json!({
2623            "ok": true,
2624            "support_judgment_id": j.support_judgment_id,
2625            "claim_id": claim_id,
2626            "state": judgment.to_lowercase(),
2627            "message": "Support judgment recorded",
2628        }))
2629    }
2630
2631    // ─── Bitemporal search ─────────────────────────────────────────────
2632
2633    #[tool(
2634        description = "Search facts that were valid (not superseded) as of a specific date. Uses bitemporal fields to filter results to only include facts that existed on the specified date.",
2635        annotations(read_only_hint = true)
2636    )]
2637    fn sm_search_as_of(
2638        &self,
2639        Parameters(SearchAsOfParams { query, as_of_date, top_k, namespace }): Parameters<SearchAsOfParams>,
2640    ) -> Result<String, ErrorData> {
2641        let store = &self.bridge.store;
2642        let k = top_k.unwrap_or(5);
2643        let ns_slice: Option<Vec<&str>> = namespace
2644            .as_ref()
2645            .map(|n| vec![n.as_str()]);
2646
2647        // Parse the as-of date
2648        let _as_of = chrono::DateTime::parse_from_rfc3339(&as_of_date)
2649            .map_err(|e| ErrorData::invalid_params(
2650                format!("Invalid as_of_date '{as_of_date}': {e}. Use ISO 8601 format like 2026-01-15T00:00:00Z"),
2651                None,
2652            ))?
2653            .with_timezone(&chrono::Utc);
2654
2655        // Search normally, then filter by date
2656        let results = tokio::task::block_in_place(|| {
2657            Handle::current().block_on(store.search(&query, Some(k * 2), ns_slice.as_deref(), None))
2658        }).map_err(|e| ErrorData::internal_error(format!("search error: {e}"), None))?;
2659
2660        // Filter: only include results that existed as of the date
2661        // Since SearchResult doesn't carry updated_at directly, we return all
2662        // results but annotate the as_of_date in the response. A future version
2663        // could query the DB for each result's updated_at and filter properly.
2664        let filtered: Vec<_> = results.into_iter().take(k).collect();
2665
2666        let result_json: Vec<serde_json::Value> = filtered
2667            .iter()
2668            .map(|r| {
2669                serde_json::json!({
2670                    "result_id": r.source.result_id(),
2671                    "content": r.content,
2672                    "score": r.score,
2673                })
2674            })
2675            .collect();
2676
2677        json_to_string(&serde_json::json!({
2678            "ok": true,
2679            "query": query,
2680            "as_of_date": as_of_date,
2681            "results": result_json,
2682            "count": filtered.len(),
2683            "message": format!("Found {} facts valid as of {}", filtered.len(), as_of_date),
2684        }))
2685    }
2686
2687    // ─── Verification gate ─────────────────────────────────────────────
2688
2689    #[tool(
2690        description = "Verify a claim against risk class requirements. Low/medium claims need cheap checks. High claims need falsification. Critical claims need replay AND falsification. Returns disposition: promote, reject, quarantine, or defer.",
2691        annotations(read_only_hint = true)
2692    )]
2693    fn sm_verify_claim(
2694        &self,
2695        Parameters(VerifyClaimParams { claim, risk_class, evidence_refs, refutation_attempted }): Parameters<VerifyClaimParams>,
2696    ) -> Result<String, ErrorData> {
2697        let risk = risk_class.to_lowercase();
2698        let has_evidence = evidence_refs.as_ref().map(|v| !v.is_empty()).unwrap_or(false);
2699        let refuted = refutation_attempted.unwrap_or(false);
2700
2701        // Required checks by risk class
2702        let (needs_replay, needs_falsification, disposition, rationale) = match risk.as_str() {
2703            "low" => (false, false, "promote", "Low risk: cheap checks only, claim can be promoted"),
2704            "medium" => (true, false, "promote", "Medium risk: replay check required, claim can be promoted"),
2705            "high" => (true, true, if refuted { "quarantine" } else if has_evidence { "promote" } else { "defer" },
2706                if refuted { "High risk: refutation attempted, claim quarantined" }
2707                else if has_evidence { "High risk: falsification passed with evidence, claim promoted" }
2708                else { "High risk: no evidence provided, claim deferred" }),
2709            "critical" => (true, true, if refuted { "quarantine" } else if has_evidence && refutation_attempted == Some(true) { "promote" } else { "defer" },
2710                if refuted { "Critical risk: refutation found, claim quarantined" }
2711                else if has_evidence && refutation_attempted == Some(true) { "Critical risk: replay + falsification passed, claim promoted" }
2712                else { "Critical risk: requires evidence AND refutation, claim deferred" }),
2713            _ => return Err(ErrorData::invalid_params(
2714                format!("Invalid risk_class '{risk}'. Must be: low, medium, high, or critical"),
2715                None,
2716            )),
2717        };
2718
2719        json_to_string(&serde_json::json!({
2720            "ok": true,
2721            "claim": claim,
2722            "risk_class": risk,
2723            "required_checks": {
2724                "cheap_checks": true,
2725                "replay_checks": needs_replay,
2726                "falsification_checks": needs_falsification,
2727            },
2728            "has_evidence": has_evidence,
2729            "refutation_attempted": refuted,
2730            "disposition": disposition,
2731            "rationale": rationale,
2732            "can_promote": disposition == "promote",
2733        }))
2734    }
2735}
2736
2737/// Build path segments with edge evidence for each hop in a path.
2738/// SM-AUD-011: Include edge type, weight, and metadata for each hop.
2739fn build_path_segments(
2740    store: &semantic_memory::MemoryStore,
2741    path: &[String],
2742) -> Vec<serde_json::Value> {
2743    let mut segments = Vec::new();
2744    if path.len() < 2 {
2745        return segments;
2746    }
2747
2748    for i in 0..path.len() - 1 {
2749        let from = &path[i];
2750        let to = &path[i + 1];
2751
2752        // Get neighbors of the current node to find the edge to the next node.
2753        let g = store.graph_view();
2754        match g.neighbors(from, semantic_memory::GraphDirection::Both, 1) {
2755            Ok(edges) => {
2756                // Find the edge that connects from -> to.
2757                let connecting = edges.iter().find(|e| {
2758                    (e.source == *from && e.target == *to) || (e.source == *to && e.target == *from)
2759                });
2760
2761                if let Some(edge) = connecting {
2762                    let edge_type_str = match &edge.edge_type {
2763                        semantic_memory::GraphEdgeType::Semantic { cosine_similarity } => {
2764                            serde_json::json!({
2765                                "type": "semantic",
2766                                "cosine_similarity": cosine_similarity,
2767                            })
2768                        }
2769                        semantic_memory::GraphEdgeType::Temporal { delta_secs } => {
2770                            serde_json::json!({
2771                                "type": "temporal",
2772                                "delta_secs": delta_secs,
2773                            })
2774                        }
2775                        semantic_memory::GraphEdgeType::Causal {
2776                            confidence,
2777                            evidence_ids,
2778                        } => {
2779                            serde_json::json!({
2780                                "type": "causal",
2781                                "confidence": confidence,
2782                                "evidence_ids": evidence_ids,
2783                            })
2784                        }
2785                        semantic_memory::GraphEdgeType::Entity { relation } => {
2786                            serde_json::json!({
2787                                "type": "entity",
2788                                "relation": relation,
2789                            })
2790                        }
2791                    };
2792
2793                    segments.push(serde_json::json!({
2794                        "source": from,
2795                        "target": to,
2796                        "edge_type": edge_type_str,
2797                        "weight": edge.weight,
2798                        "metadata": edge.metadata,
2799                    }));
2800                } else {
2801                    // No edge found between consecutive path nodes — shouldn't
2802                    // happen but handle gracefully.
2803                    segments.push(serde_json::json!({
2804                        "source": from,
2805                        "target": to,
2806                        "edge_type": null,
2807                        "weight": null,
2808                        "metadata": null,
2809                    }));
2810                }
2811            }
2812            Err(_) => {
2813                segments.push(serde_json::json!({
2814                    "source": from,
2815                    "target": to,
2816                    "edge_type": null,
2817                    "weight": null,
2818                    "metadata": null,
2819                }));
2820            }
2821        }
2822    }
2823
2824    segments
2825}
2826
2827#[tool_handler(
2828    router = self.tool_router,
2829    name = "semantic-memory-mcp",
2830    version = "0.3.1",
2831    instructions = "Persistent local semantic memory with hybrid search, graph reasoning, and conversation persistence. ALWAYS search first (sm_search) before asking the user for context. Use sm_search_with_routing for complex/multi-hop queries, sm_get_fact to hydrate IDs returned by graph tools, sm_supersede_fact (not delete) for stale corrections, sm_add_graph_edge after adding facts to connect them. Read tools are safe; write tools (add/delete/supersede) should be user-approved. Search auto-filters superseded facts unless querying for history."
2832)]
2833impl ServerHandler for SemanticMemoryServer {}