Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions memoria/crates/memoria-api/tests/api_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3832,11 +3832,20 @@ async fn test_remote_capabilities() {
.await
.expect("capabilities");
let text = r["content"][0]["text"].as_str().unwrap_or("");
assert!(
text.contains("remote mode"),
"should mention remote mode, got: {text}"
let parsed: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("capabilities text should be JSON ({e}), got: {text}"));
assert_eq!(
parsed, r["structuredContent"],
"text and structuredContent should carry the same catalog"
);
let mut advertised = memoria_mcp::tools::list().as_array().unwrap().clone();
advertised.extend(memoria_mcp::git_tools::list().as_array().unwrap().clone());
assert_eq!(
parsed,
json!({ "tools": advertised }),
"remote capabilities should match the advertised tool catalog"
);
println!("✅ remote capabilities: {}", &text[..text.len().min(80)]);
println!("✅ remote capabilities: {} tools", advertised.len());
}

#[tokio::test]
Expand Down
5 changes: 1 addition & 4 deletions memoria/crates/memoria-mcp/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,7 @@ impl RemoteClient {
Ok(Self::mcp_text(&text))
}

"memory_capabilities" => Ok(Self::mcp_text(&format!(
"{}\n[remote mode — connected to Memoria API server]",
crate::tools::MEMORY_CAPABILITIES_TEXT
))),
"memory_capabilities" => Ok(crate::tools::capabilities_result()),

"memory_get_retrieval_params" => {
let r = self
Expand Down
8 changes: 2 additions & 6 deletions memoria/crates/memoria-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,7 @@ async fn dispatch(
})),
RpcMethod::Ping => Ok(json!({})),
RpcMethod::ToolsList => {
let mut all_tools = tools::list().as_array().unwrap().clone();
all_tools.extend(git_tools::list().as_array().unwrap().clone());
Ok(json!({"tools": all_tools}))
Ok(tools::tool_catalog())
}
RpcMethod::ToolsCall => {
let (name, args) = validate_tool_call(Some(&p))?;
Expand Down Expand Up @@ -453,9 +451,7 @@ async fn dispatch_embedded_owned(
})),
RpcMethod::Ping => Ok(json!({})),
RpcMethod::ToolsList => {
let mut all_tools = tools::list().as_array().unwrap().clone();
all_tools.extend(git_tools::list().as_array().unwrap().clone());
Ok(json!({"tools": all_tools}))
Ok(tools::tool_catalog())
}
RpcMethod::ToolsCall => {
let (name, args) = validate_tool_call(Some(&p))?;
Expand Down
53 changes: 40 additions & 13 deletions memoria/crates/memoria-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,23 @@ const TRUST_TIER_DESCRIPTION: &str = concat!(
"T4 = speculative, reflective, or otherwise unverified hypothesis."
);

pub(crate) const MEMORY_CAPABILITIES_TEXT: &str = concat!(
"Available tools: memory_store, memory_retrieve, memory_search, ",
"memory_correct, memory_purge, memory_profile, memory_list, ",
"memory_capabilities, memory_governance, memory_consolidate, ",
"memory_reflect, memory_feedback",
"\n\nmemory_store trust_tier guide:",
"\n- T1 (Verified): directly stated or explicitly confirmed facts, preferences, or decisions.",
"\n- T2 (Curated): corrected or manually curated memory replacing an older record.",
"\n- T3 (Inferred): summaries, extracted patterns, or soft conclusions not explicitly confirmed. Prefer T3 if unsure.",
"\n- T4 (Unverified): speculative, reflective, or otherwise unverified hypotheses.",
"\nUse exact values T1/T2/T3/T4; natural-language labels like 'verified' are invalid."
);
/// One catalog for tools/list and memory_capabilities, including Git-backed tools.
pub(crate) fn tool_catalog() -> Value {
let mut all_tools = list().as_array().unwrap().clone();
all_tools.extend(crate::git_tools::list().as_array().unwrap().clone());
json!({"tools": all_tools})
}

/// Return machine-readable MCP tool discovery while keeping JSON text content
/// for clients that do not yet consume structuredContent.
pub(crate) fn capabilities_result() -> Value {
let catalog = tool_catalog();
let text = catalog.to_string();
json!({
"content": [{"type": "text", "text": text}],
"structuredContent": catalog
})
}

pub fn list() -> Value {
json!([
Expand Down Expand Up @@ -796,7 +801,7 @@ pub async fn call(
Ok(mcp_text(&text))
}

ToolCallName::MemoryCapabilities => Ok(mcp_text(MEMORY_CAPABILITIES_TEXT)),
ToolCallName::MemoryCapabilities => Ok(capabilities_result()),

ToolCallName::MemoryGovernance => {
let force = args["force"].as_bool().unwrap_or(false);
Expand Down Expand Up @@ -1573,6 +1578,28 @@ pub fn entity_extract_prompt(text: &str) -> String {
mod tests {
use super::*;

#[test]
fn memory_capabilities_match_the_public_mcp_catalog() {
let result = capabilities_result();
let text = result["content"][0]["text"].as_str().unwrap();
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(parsed, result["structuredContent"]);
assert_eq!(parsed, tool_catalog());
let tools = parsed["tools"].as_array().unwrap();
for required in ["memory_store", "memory_capabilities", "memory_checkout"] {
assert!(
tools.iter().any(|tool| tool["name"].as_str() == Some(required)),
"missing advertised tool: {required}"
);
}
for tool in tools {
assert!(tool["name"].is_string());
assert!(tool["description"].is_string());
assert!(tool["inputSchema"].is_object());
}
}


#[test]
fn missing_session_id_preserves_message_and_input_classification() {
for args in [
Expand Down
58 changes: 45 additions & 13 deletions memoria/crates/memoria-mcp/tests/core_tools_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,25 @@ async fn test_capabilities() {
let (svc, uid, _ctx) = setup().await;
let r = call("memory_capabilities", json!({}), &svc, &uid).await;
let t = text(&r);
let parsed: Value = serde_json::from_str(t)
.unwrap_or_else(|e| panic!("capabilities text should be JSON ({e}), got: {t}"));
assert_eq!(
parsed, r["structuredContent"],
"text and structuredContent should carry the same catalog"
);
let mut advertised = memoria_mcp::tools::list().as_array().unwrap().clone();
advertised.extend(memoria_mcp::git_tools::list().as_array().unwrap().clone());
assert_eq!(
parsed,
json!({ "tools": advertised }),
"capabilities should match the advertised tool catalog"
);
let names: Vec<&str> = parsed["tools"]
.as_array()
.unwrap()
.iter()
.filter_map(|tool| tool["name"].as_str())
.collect();
for tool in &[
"memory_store",
"memory_retrieve",
Expand All @@ -857,34 +876,47 @@ async fn test_capabilities() {
"memory_purge",
"memory_profile",
"memory_feedback",
"memory_observe",
] {
assert!(t.contains(tool), "missing {tool}: {t}");
assert!(names.contains(tool), "missing {tool}: {names:?}");
}
// Hidden ops/debug tools must NOT appear in capabilities
// Unadvertised ops/debug tools must NOT appear in capabilities
for hidden in &[
"memory_rebuild_index",
"memory_extract_entities",
"memory_link_entities",
"memory_get_retrieval_params",
"memory_tune_params",
"memory_observe",
] {
assert!(
!t.contains(hidden),
"hidden tool {hidden} should not be in capabilities: {t}"
!names.contains(hidden),
"hidden tool {hidden} should not be in capabilities: {names:?}"
);
}
// Trust-tier guidance now lives in the memory_store input schema.
let store = parsed["tools"]
.as_array()
.unwrap()
.iter()
.find(|tool| tool["name"] == "memory_store")
.expect("memory_store advertised");
let tier_desc = store["inputSchema"]["properties"]["trust_tier"]["description"]
.as_str()
.expect("trust_tier description");
for hint in &[
"memory_store trust_tier guide",
"T1 (Verified)",
"T2 (Curated)",
"T3 (Inferred)",
"Prefer T3 if unsure",
"natural-language labels like 'verified' are invalid",
"T1/T2/T3/T4",
"T1 =",
"T2 =",
"T3 =",
"prefer T3 if unsure",
"T4 =",
] {
assert!(t.contains(hint), "missing tier hint {hint}: {t}");
assert!(
tier_desc.contains(hint),
"missing tier hint {hint}: {tier_desc}"
);
}
println!("✅ capabilities: {t}");
println!("✅ capabilities: {} tools", names.len());
}

// ── 18. unknown tool returns error ────────────────────────────────────────────
Expand Down
Loading