velesdb_memory/mcp/
status.rs1use std::sync::Arc;
4
5use rmcp::handler::server::wrapper::Json;
6use rmcp::{tool, tool_router, ErrorData};
7
8use super::dto::{
9 EmbedderStatus, ExtractionStatus, MemoryCounts, MemoryStatusResult, ProvenanceStatus,
10};
11use super::{join_error, to_error, McpServer, UNREPORTED_MODEL};
12use crate::embedding_provenance::EmbeddingProvenance;
13
14type StatusSnapshot = (
15 String,
16 usize,
17 Option<EmbeddingProvenance>,
18 usize,
19 Option<usize>,
20 bool,
21 u64,
22);
23
24#[tool_router(router = status_tool_router, vis = "pub(super)")]
25impl McpServer {
26 #[tool(
27 name = "memory_status",
28 output_schema = crate::schema::wire_safe_output_schema::<MemoryStatusResult>(),
29 description = "Report this memory server's health and configuration: which embedder is running and whether recall is SEMANTIC (`embedder.semantic: false` means the offline `hash` default — recall matches surface form, not meaning, and configuring a semantic embedder is an env-var switch, no rebuild), what embedder the store was filled by per its on-disk provenance record, whether a default extraction backend is configured (`remember_extracted` may omit `extractor` iff `extraction.configured`; explicit `outline` remains available), whether the background autograph worker is active and how many enrichments a full queue dropped, and the corpus size — `memory.facts` and `memory.edges`. Read `memory.edges` when `why` seems to add nothing over `recall`: `0` means no fact was ever linked (by `relate`, `remember`'s `links`, or extraction), so `why` HAS no graph to walk and degrades to plain search — that is a wiring gap, not a defect. Call this at session start, or whenever recall quality or `why`'s evidence trails surprise you, and tell the user when the server runs degraded. Takes no parameters."
30 )]
31 async fn memory_status(&self) -> Result<Json<MemoryStatusResult>, ErrorData> {
32 let service = Arc::clone(&self.service);
33 let store_dir = self.store_dir.clone();
34 let snapshot = tokio::task::spawn_blocking(move || {
35 let recorded = store_dir
36 .as_deref()
37 .and_then(|dir| crate::embedding_provenance::read(dir).ok().flatten());
38 service.inspect_active(|model, dimension, current| {
39 (
40 model.to_owned(),
41 dimension,
42 recorded,
43 current.fact_count(),
44 current.edge_count(),
45 current.autograph_queue_open(),
46 current.autograph_dropped(),
47 )
48 })
49 })
50 .await
51 .map_err(join_error)?
52 .map_err(to_error)?;
53 Ok(Json(self.status_result(snapshot)))
54 }
55
56 fn status_result(&self, snapshot: StatusSnapshot) -> MemoryStatusResult {
57 let (model, dimension, provenance, facts, edges, autograph_active, autograph_dropped) =
58 snapshot;
59 MemoryStatusResult {
60 embedder: embedder_status(model, dimension),
61 provenance: provenance_status(provenance),
62 extraction: ExtractionStatus {
63 configured: self.extractors.read().default_is_configured(),
64 autograph_active,
65 autograph_dropped,
66 },
67 memory: MemoryCounts { facts, edges },
68 }
69 }
70}
71
72fn embedder_status(model: String, dimension: usize) -> EmbedderStatus {
73 if model == UNREPORTED_MODEL {
74 return EmbedderStatus {
75 model: None,
76 dimension: None,
77 semantic: None,
78 };
79 }
80 EmbedderStatus {
81 semantic: Some(model != "hash"),
82 model: Some(model),
83 dimension: Some(dimension),
84 }
85}
86
87fn provenance_status(record: Option<EmbeddingProvenance>) -> ProvenanceStatus {
88 record.map_or(
89 ProvenanceStatus {
90 recorded: false,
91 model: None,
92 dimension: None,
93 },
94 |record| ProvenanceStatus {
95 recorded: true,
96 model: Some(record.model),
97 dimension: Some(record.dimension),
98 },
99 )
100}