codex_memory/mcp_server/
tools.rs

1//! MCP Tools Definition and Schema
2//!
3//! This module defines the tools exposed through the MCP protocol,
4//! including their schemas and capabilities for memory management.
5
6use serde_json::{json, Value};
7
8/// MCP Tools registry and schema definitions
9pub struct MCPTools;
10
11impl MCPTools {
12    /// Get the list of available tools with their schemas
13    pub fn get_tools_list() -> Value {
14        json!({
15            "tools": [
16                {
17                    "name": "store_memory",
18                    "description": "Store a memory in the hierarchical memory system",
19                    "inputSchema": {
20                        "type": "object",
21                        "properties": {
22                            "content": {
23                                "type": "string",
24                                "description": "The content to store as a memory"
25                            },
26                            "tier": {
27                                "type": "string",
28                                "enum": ["working", "warm", "cold"],
29                                "description": "The tier to store the memory in (defaults to automatic placement)",
30                                "default": "working"
31                            },
32                            "tags": {
33                                "type": "array",
34                                "items": {"type": "string"},
35                                "description": "Optional tags to associate with the memory for categorization"
36                            },
37                            "importance_score": {
38                                "type": "number",
39                                "minimum": 0.0,
40                                "maximum": 1.0,
41                                "description": "Optional importance score (0.0 to 1.0) for the memory"
42                            },
43                            "metadata": {
44                                "type": "object",
45                                "description": "Optional additional metadata to store with the memory"
46                            }
47                        },
48                        "required": ["content"]
49                    }
50                },
51                {
52                    "name": "search_memory",
53                    "description": "Search memories using semantic similarity",
54                    "inputSchema": {
55                        "type": "object",
56                        "properties": {
57                            "query": {
58                                "type": "string",
59                                "description": "The search query text"
60                            },
61                            "limit": {
62                                "type": "integer",
63                                "default": 10,
64                                "minimum": 1,
65                                "maximum": 100,
66                                "description": "Maximum number of results to return"
67                            },
68                            "similarity_threshold": {
69                                "type": "number",
70                                "minimum": 0.0,
71                                "maximum": 1.0,
72                                "default": 0.5,
73                                "description": "Minimum similarity score for results"
74                            },
75                            "tier": {
76                                "type": "string",
77                                "enum": ["working", "warm", "cold"],
78                                "description": "Optional tier filter to search within"
79                            },
80                            "tags": {
81                                "type": "array",
82                                "items": {"type": "string"},
83                                "description": "Optional tags to filter results by"
84                            },
85                            "include_metadata": {
86                                "type": "boolean",
87                                "default": true,
88                                "description": "Whether to include metadata in results"
89                            }
90                        },
91                        "required": ["query"]
92                    }
93                },
94                {
95                    "name": "get_statistics",
96                    "description": "Get memory system statistics and metrics",
97                    "inputSchema": {
98                        "type": "object",
99                        "properties": {
100                            "detailed": {
101                                "type": "boolean",
102                                "default": false,
103                                "description": "Whether to include detailed metrics"
104                            }
105                        },
106                        "required": []
107                    }
108                },
109                {
110                    "name": "what_did_you_remember",
111                    "description": "Query what the system has remembered about recent interactions",
112                    "inputSchema": {
113                        "type": "object",
114                        "properties": {
115                            "context": {
116                                "type": "string",
117                                "description": "Optional context to filter memories by (e.g., 'conversation', 'project')",
118                                "default": "conversation"
119                            },
120                            "time_range": {
121                                "type": "string",
122                                "enum": ["last_hour", "last_day", "last_week", "last_month"],
123                                "description": "Time range to search within",
124                                "default": "last_day"
125                            },
126                            "limit": {
127                                "type": "integer",
128                                "minimum": 1,
129                                "maximum": 50,
130                                "default": 10,
131                                "description": "Maximum number of memories to return"
132                            }
133                        },
134                        "required": []
135                    }
136                },
137                {
138                    "name": "harvest_conversation",
139                    "description": "Trigger the silent harvester to process current conversation context",
140                    "inputSchema": {
141                        "type": "object",
142                        "properties": {
143                            "message": {
144                                "type": "string",
145                                "description": "Message content to harvest"
146                            },
147                            "context": {
148                                "type": "string",
149                                "description": "Context for the message (e.g., 'conversation', 'project')",
150                                "default": "conversation"
151                            },
152                            "role": {
153                                "type": "string",
154                                "enum": ["user", "assistant", "system"],
155                                "description": "Role of the message sender",
156                                "default": "user"
157                            },
158                            "force_harvest": {
159                                "type": "boolean",
160                                "default": false,
161                                "description": "Force immediate harvest instead of queuing"
162                            },
163                            "silent_mode": {
164                                "type": "boolean",
165                                "default": true,
166                                "description": "Run in silent mode (minimal output)"
167                            }
168                        },
169                        "required": []
170                    }
171                },
172                {
173                    "name": "get_harvester_metrics",
174                    "description": "Get metrics and status from the silent harvester service",
175                    "inputSchema": {
176                        "type": "object",
177                        "properties": {},
178                        "required": []
179                    }
180                },
181                {
182                    "name": "migrate_memory",
183                    "description": "Move a memory between tiers in the hierarchical system",
184                    "inputSchema": {
185                        "type": "object",
186                        "properties": {
187                            "memory_id": {
188                                "type": "string",
189                                "description": "UUID of the memory to migrate"
190                            },
191                            "target_tier": {
192                                "type": "string",
193                                "enum": ["working", "warm", "cold", "frozen"],
194                                "description": "Target tier for migration"
195                            },
196                            "reason": {
197                                "type": "string",
198                                "description": "Optional reason for migration"
199                            }
200                        },
201                        "required": ["memory_id", "target_tier"]
202                    }
203                },
204                {
205                    "name": "delete_memory",
206                    "description": "Delete a specific memory from the system",
207                    "inputSchema": {
208                        "type": "object",
209                        "properties": {
210                            "memory_id": {
211                                "type": "string",
212                                "description": "UUID of the memory to delete"
213                            },
214                            "confirm": {
215                                "type": "boolean",
216                                "default": false,
217                                "description": "Confirmation flag to prevent accidental deletions"
218                            }
219                        },
220                        "required": ["memory_id", "confirm"]
221                    }
222                }
223            ]
224        })
225    }
226
227    /// Get empty resources list (codex-memory doesn't expose resources)
228    pub fn get_resources_list() -> Value {
229        json!({
230            "resources": []
231        })
232    }
233
234    /// Get empty prompts list (codex-memory doesn't use prompts)
235    pub fn get_prompts_list() -> Value {
236        json!({
237            "prompts": []
238        })
239    }
240
241    /// Get server capabilities for MCP initialization
242    pub fn get_server_capabilities() -> Value {
243        json!({
244            "protocolVersion": "2025-06-18",
245            "capabilities": {
246                "tools": {
247                    "listChanged": false
248                },
249                "resources": {
250                    "listChanged": false
251                },
252                "prompts": {
253                    "listChanged": false
254                }
255            },
256            "serverInfo": {
257                "name": "codex-memory",
258                "version": env!("CARGO_PKG_VERSION"),
259                "description": "Hierarchical memory system with semantic search and automated consolidation"
260            }
261        })
262    }
263
264    /// Validate tool arguments against schema
265    pub fn validate_tool_args(tool_name: &str, args: &Value) -> Result<(), String> {
266        match tool_name {
267            "store_memory" => {
268                if args
269                    .get("content")
270                    .and_then(|c| c.as_str())
271                    .is_none_or(|s| s.is_empty())
272                {
273                    return Err("Content is required and cannot be empty".to_string());
274                }
275
276                // Validate tier if provided
277                if let Some(tier) = args.get("tier").and_then(|t| t.as_str()) {
278                    if !["working", "warm", "cold"].contains(&tier) {
279                        return Err(
280                            "Invalid tier. Must be 'working', 'warm', or 'cold'".to_string()
281                        );
282                    }
283                }
284
285                // Validate importance score if provided
286                if let Some(score) = args.get("importance_score").and_then(|s| s.as_f64()) {
287                    if !(0.0..=1.0).contains(&score) {
288                        return Err("Importance score must be between 0.0 and 1.0".to_string());
289                    }
290                }
291            }
292            "search_memory" => {
293                if args
294                    .get("query")
295                    .and_then(|q| q.as_str())
296                    .is_none_or(|s| s.is_empty())
297                {
298                    return Err("Query is required and cannot be empty".to_string());
299                }
300
301                // Validate limit if provided
302                if let Some(limit) = args.get("limit").and_then(|l| l.as_i64()) {
303                    if !(1..=100).contains(&limit) {
304                        return Err("Limit must be between 1 and 100".to_string());
305                    }
306                }
307
308                // Validate similarity threshold if provided
309                if let Some(threshold) = args.get("similarity_threshold").and_then(|t| t.as_f64()) {
310                    if !(0.0..=1.0).contains(&threshold) {
311                        return Err("Similarity threshold must be between 0.0 and 1.0".to_string());
312                    }
313                }
314            }
315            "migrate_memory" => {
316                if args
317                    .get("memory_id")
318                    .and_then(|id| id.as_str())
319                    .is_none_or(|s| s.is_empty())
320                {
321                    return Err("Memory ID is required".to_string());
322                }
323
324                if let Some(tier) = args.get("target_tier").and_then(|t| t.as_str()) {
325                    if !["working", "warm", "cold", "frozen"].contains(&tier) {
326                        return Err("Invalid target tier".to_string());
327                    }
328                } else {
329                    return Err("Target tier is required".to_string());
330                }
331            }
332            "delete_memory" => {
333                if args
334                    .get("memory_id")
335                    .and_then(|id| id.as_str())
336                    .is_none_or(|s| s.is_empty())
337                {
338                    return Err("Memory ID is required".to_string());
339                }
340
341                if !args
342                    .get("confirm")
343                    .and_then(|c| c.as_bool())
344                    .unwrap_or(false)
345                {
346                    return Err("Confirmation required for deletion".to_string());
347                }
348            }
349            "what_did_you_remember" => {
350                // Validate time_range if provided
351                if let Some(range) = args.get("time_range").and_then(|r| r.as_str()) {
352                    if !["last_hour", "last_day", "last_week", "last_month"].contains(&range) {
353                        return Err("Invalid time range".to_string());
354                    }
355                }
356            }
357            "harvest_conversation" => {
358                // Validate role if provided
359                if let Some(role) = args.get("role").and_then(|r| r.as_str()) {
360                    if !["user", "assistant", "system"].contains(&role) {
361                        return Err(
362                            "Invalid role. Must be 'user', 'assistant', or 'system'".to_string()
363                        );
364                    }
365                }
366            }
367            "get_statistics" | "get_harvester_metrics" => {
368                // These tools don't require validation
369            }
370            _ => return Err(format!("Unknown tool: {tool_name}")),
371        }
372
373        Ok(())
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_tools_list_structure() {
383        let tools = MCPTools::get_tools_list();
384        let tools_array = tools["tools"].as_array().unwrap();
385
386        assert!(!tools_array.is_empty());
387
388        // Check that store_memory tool exists with required schema
389        let store_memory = tools_array
390            .iter()
391            .find(|t| t["name"] == "store_memory")
392            .unwrap();
393
394        assert_eq!(store_memory["name"], "store_memory");
395        assert!(store_memory["inputSchema"]["properties"]["content"].is_object());
396        assert_eq!(store_memory["inputSchema"]["required"][0], "content");
397    }
398
399    #[test]
400    fn test_tool_validation() {
401        // Test valid store_memory args
402        let valid_args = json!({
403            "content": "Test memory",
404            "tier": "working",
405            "importance_score": 0.8
406        });
407        assert!(MCPTools::validate_tool_args("store_memory", &valid_args).is_ok());
408
409        // Test invalid content
410        let invalid_args = json!({
411            "content": "",
412            "tier": "working"
413        });
414        assert!(MCPTools::validate_tool_args("store_memory", &invalid_args).is_err());
415
416        // Test invalid tier
417        let invalid_tier = json!({
418            "content": "Test",
419            "tier": "invalid"
420        });
421        assert!(MCPTools::validate_tool_args("store_memory", &invalid_tier).is_err());
422
423        // Test unknown tool
424        assert!(MCPTools::validate_tool_args("unknown_tool", &valid_args).is_err());
425    }
426
427    #[test]
428    fn test_server_capabilities() {
429        let capabilities = MCPTools::get_server_capabilities();
430        assert_eq!(capabilities["protocolVersion"], "2025-06-18");
431        assert_eq!(capabilities["serverInfo"]["name"], "codex-memory");
432        assert!(capabilities["capabilities"]["tools"]["listChanged"] == false);
433    }
434}