trusty_memory/service.rs
1//! `ServiceDescriptor` impl for the trusty-memory MCP service.
2//!
3//! Why: open-mpm (and any future host process that links several MCP
4//! services into a single binary) needs a uniform way to enumerate every
5//! tool a linked service contributes and the scopes each tool requires.
6//! The shared `trusty_mcp_core::ServiceDescriptor` trait is that contract:
7//! the host collects `&dyn ServiceDescriptor` impls and feeds them to
8//! `OpenRpcBuilder::from_services` to emit one merged `rpc.discover`
9//! document covering all of them. By implementing the trait here, the
10//! host no longer needs to know anything concrete about trusty-memory.
11//!
12//! What: A zero-sized `MemoryMcpService` struct that delegates to the
13//! existing `tool_definitions_with` and `scopes_for_tool` helpers already
14//! used by this crate's standalone `rpc.discover` handler. Reusing those
15//! functions guarantees the host-merged manifest and the standalone one
16//! stay byte-identical.
17//!
18//! Test: `tests` below assert the tool count (11), the name string, and
19//! that the read/write scope split matches what `scopes_for_tool` returns
20//! for representative tools (`memory_recall` → `memory.read`,
21//! `memory_remember` → `memory.write`).
22
23use trusty_common::mcp::ServiceDescriptor;
24
25use crate::openrpc::scopes_for_tool;
26use crate::tools::tool_definitions_with;
27
28/// `ServiceDescriptor` impl that advertises this crate's 11 memory tools.
29///
30/// Why: Lets open-mpm link trusty-memory-mcp directly and include its
31/// tools in a unified `rpc.discover` document without bespoke glue code.
32/// What: Wraps the existing tool definitions and the per-tool scope
33/// mapping behind the shared trait. The struct is unit-like — there is
34/// no per-instance state — so callers can construct it with
35/// `MemoryMcpService` at the call site.
36/// Test: `tests::tools_returns_eleven`, `tests::scopes_for_read_tool`,
37/// `tests::scopes_for_write_tool`, `tests::name_returns_trusty_memory`.
38#[derive(Debug, Default, Clone, Copy)]
39pub struct MemoryMcpService;
40
41impl ServiceDescriptor for MemoryMcpService {
42 fn name(&self) -> &str {
43 "trusty-memory"
44 }
45
46 fn version(&self) -> &str {
47 env!("CARGO_PKG_VERSION")
48 }
49
50 fn tools(&self) -> Vec<serde_json::Value> {
51 // Why: `tool_definitions_with(false)` matches the schema clients see
52 // when no default palace is configured — the conservative shape
53 // where `palace` is a required argument on every palace-scoped
54 // tool. The host can override later if it wants to surface the
55 // `has_default = true` variant.
56 let defs = tool_definitions_with(false);
57 defs.get("tools")
58 .and_then(|v| v.as_array())
59 .cloned()
60 .unwrap_or_default()
61 }
62
63 fn scopes_for(&self, tool: &str) -> Vec<String> {
64 scopes_for_tool(tool)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn name_returns_trusty_memory() {
74 let svc = MemoryMcpService;
75 assert_eq!(svc.name(), "trusty-memory");
76 }
77
78 #[test]
79 fn version_matches_cargo_pkg_version() {
80 let svc = MemoryMcpService;
81 assert_eq!(svc.version(), env!("CARGO_PKG_VERSION"));
82 }
83
84 #[test]
85 fn tools_returns_eleven() {
86 let svc = MemoryMcpService;
87 let tools = svc.tools();
88 assert_eq!(
89 tools.len(),
90 12,
91 "expected 12 memory tools, got {}",
92 tools.len()
93 );
94 }
95
96 #[test]
97 fn scopes_for_read_tool() {
98 let svc = MemoryMcpService;
99 assert_eq!(svc.scopes_for("memory_recall"), vec!["memory.read"]);
100 }
101
102 #[test]
103 fn scopes_for_write_tool() {
104 let svc = MemoryMcpService;
105 assert_eq!(svc.scopes_for("memory_remember"), vec!["memory.write"]);
106 }
107
108 #[test]
109 fn dispatches_through_trait_object() {
110 // Why: open-mpm collects services as `Vec<Box<dyn ServiceDescriptor>>`,
111 // so we must confirm dynamic dispatch resolves correctly here.
112 let svc: Box<dyn ServiceDescriptor> = Box::new(MemoryMcpService);
113 assert_eq!(svc.name(), "trusty-memory");
114 assert_eq!(svc.tools().len(), 12);
115 assert_eq!(svc.scopes_for("palace_create"), vec!["memory.write"]);
116 assert_eq!(svc.scopes_for("palace_list"), vec!["memory.read"]);
117 }
118}