Skip to main content

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}