Skip to main content

reddb_server/mcp/
tools.rs

1//! MCP tool definitions for RedDB.
2//!
3//! Each tool exposes a specific RedDB capability to AI agents with a
4//! typed JSON Schema input specification.
5
6use crate::json::{Map, Value as JsonValue};
7
8/// Definition of an MCP tool exposed by the RedDB server.
9pub struct ToolDef {
10    pub name: &'static str,
11    pub description: &'static str,
12    pub input_schema: JsonValue,
13}
14
15/// Build a JSON Schema object from a list of field descriptors.
16fn schema(properties: Vec<(&str, &str, &str)>, required: Vec<&str>) -> JsonValue {
17    let mut props = Map::new();
18    for (name, field_type, description) in properties {
19        let mut field = Map::new();
20        field.insert(
21            "type".to_string(),
22            JsonValue::String(field_type.to_string()),
23        );
24        if !description.is_empty() {
25            field.insert(
26                "description".to_string(),
27                JsonValue::String(description.to_string()),
28            );
29        }
30        props.insert(name.to_string(), JsonValue::Object(field));
31    }
32
33    let mut obj = Map::new();
34    obj.insert("type".to_string(), JsonValue::String("object".to_string()));
35    obj.insert("properties".to_string(), JsonValue::Object(props));
36    obj.insert(
37        "required".to_string(),
38        JsonValue::Array(
39            required
40                .into_iter()
41                .map(|s| JsonValue::String(s.to_string()))
42                .collect(),
43        ),
44    );
45    obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
46    JsonValue::Object(obj)
47}
48
49/// Build a JSON Schema object that accepts items with flexible inner types.
50fn schema_with_nested(properties: Vec<(&str, JsonValue)>, required: Vec<&str>) -> JsonValue {
51    let mut props = Map::new();
52    for (name, descriptor) in properties {
53        props.insert(name.to_string(), descriptor);
54    }
55
56    let mut obj = Map::new();
57    obj.insert("type".to_string(), JsonValue::String("object".to_string()));
58    obj.insert("properties".to_string(), JsonValue::Object(props));
59    obj.insert(
60        "required".to_string(),
61        JsonValue::Array(
62            required
63                .into_iter()
64                .map(|s| JsonValue::String(s.to_string()))
65                .collect(),
66        ),
67    );
68    obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
69    JsonValue::Object(obj)
70}
71
72/// Simple string field descriptor.
73fn string_field(description: &str) -> JsonValue {
74    let mut f = Map::new();
75    f.insert("type".to_string(), JsonValue::String("string".to_string()));
76    f.insert(
77        "description".to_string(),
78        JsonValue::String(description.to_string()),
79    );
80    JsonValue::Object(f)
81}
82
83/// Simple number field descriptor.
84fn number_field(description: &str) -> JsonValue {
85    let mut f = Map::new();
86    f.insert("type".to_string(), JsonValue::String("number".to_string()));
87    f.insert(
88        "description".to_string(),
89        JsonValue::String(description.to_string()),
90    );
91    JsonValue::Object(f)
92}
93
94/// Simple integer field descriptor.
95fn integer_field(description: &str) -> JsonValue {
96    let mut f = Map::new();
97    f.insert("type".to_string(), JsonValue::String("integer".to_string()));
98    f.insert(
99        "description".to_string(),
100        JsonValue::String(description.to_string()),
101    );
102    JsonValue::Object(f)
103}
104
105/// Simple boolean field descriptor.
106fn boolean_field(description: &str) -> JsonValue {
107    let mut f = Map::new();
108    f.insert("type".to_string(), JsonValue::String("boolean".to_string()));
109    f.insert(
110        "description".to_string(),
111        JsonValue::String(description.to_string()),
112    );
113    JsonValue::Object(f)
114}
115
116/// Object field descriptor (accepts arbitrary JSON object).
117fn object_field(description: &str) -> JsonValue {
118    let mut f = Map::new();
119    f.insert("type".to_string(), JsonValue::String("object".to_string()));
120    f.insert(
121        "description".to_string(),
122        JsonValue::String(description.to_string()),
123    );
124    JsonValue::Object(f)
125}
126
127/// Array-of-numbers field descriptor.
128fn number_array_field(description: &str) -> JsonValue {
129    let mut items = Map::new();
130    items.insert("type".to_string(), JsonValue::String("number".to_string()));
131
132    let mut f = Map::new();
133    f.insert("type".to_string(), JsonValue::String("array".to_string()));
134    f.insert("items".to_string(), JsonValue::Object(items));
135    f.insert(
136        "description".to_string(),
137        JsonValue::String(description.to_string()),
138    );
139    JsonValue::Object(f)
140}
141
142/// Array-of-strings field descriptor.
143fn string_array_field(description: &str) -> JsonValue {
144    let mut items = Map::new();
145    items.insert("type".to_string(), JsonValue::String("string".to_string()));
146
147    let mut f = Map::new();
148    f.insert("type".to_string(), JsonValue::String("array".to_string()));
149    f.insert("items".to_string(), JsonValue::Object(items));
150    f.insert(
151        "description".to_string(),
152        JsonValue::String(description.to_string()),
153    );
154    JsonValue::Object(f)
155}
156
157/// Return all tool definitions exposed by the RedDB MCP server.
158pub fn all_tools() -> Vec<ToolDef> {
159    vec![
160        ToolDef {
161            name: "reddb_query",
162            description: "Execute a SQL or universal query against RedDB. Supports SELECT, INSERT, UPDATE, DELETE, and graph queries (Gremlin, Cypher, SPARQL).",
163            input_schema: schema(
164                vec![("sql", "string", "SQL or universal query to execute")],
165                vec!["sql"],
166            ),
167        },
168        ToolDef {
169            name: "reddb_collections",
170            description: "List all collections in the database.",
171            input_schema: schema(vec![], vec![]),
172        },
173        ToolDef {
174            name: "reddb_insert_row",
175            description: "Insert a table row into a collection.",
176            input_schema: schema_with_nested(
177                vec![
178                    ("collection", string_field("Target collection name")),
179                    ("data", object_field("Object with field name/value pairs to insert")),
180                    ("metadata", object_field("Optional metadata key/value pairs")),
181                ],
182                vec!["collection", "data"],
183            ),
184        },
185        ToolDef {
186            name: "reddb_insert_node",
187            description: "Insert a graph node into a collection.",
188            input_schema: schema_with_nested(
189                vec![
190                    ("collection", string_field("Target collection name")),
191                    ("label", string_field("Node label (identifier)")),
192                    ("node_type", string_field("Optional node type classification")),
193                    ("properties", object_field("Optional node properties as key/value pairs")),
194                    ("metadata", object_field("Optional metadata key/value pairs")),
195                ],
196                vec!["collection", "label"],
197            ),
198        },
199        ToolDef {
200            name: "reddb_insert_edge",
201            description: "Insert a graph edge between two nodes.",
202            input_schema: schema_with_nested(
203                vec![
204                    ("collection", string_field("Target collection name")),
205                    ("label", string_field("Edge label (relationship type)")),
206                    ("from", integer_field("Source node entity ID")),
207                    ("to", integer_field("Target node entity ID")),
208                    ("weight", number_field("Optional edge weight (default 1.0)")),
209                    ("properties", object_field("Optional edge properties")),
210                    ("metadata", object_field("Optional metadata key/value pairs")),
211                ],
212                vec!["collection", "label", "from", "to"],
213            ),
214        },
215        ToolDef {
216            name: "reddb_insert_vector",
217            description: "Insert a vector embedding into a collection.",
218            input_schema: schema_with_nested(
219                vec![
220                    ("collection", string_field("Target collection name")),
221                    ("dense", number_array_field("Dense vector (array of floats)")),
222                    ("content", string_field("Optional text content associated with the vector")),
223                    ("metadata", object_field("Optional metadata key/value pairs")),
224                ],
225                vec!["collection", "dense"],
226            ),
227        },
228        ToolDef {
229            name: "reddb_insert_document",
230            description: "Insert a JSON document into a collection.",
231            input_schema: schema_with_nested(
232                vec![
233                    ("collection", string_field("Target collection name")),
234                    ("body", object_field("JSON document body")),
235                    ("metadata", object_field("Optional metadata key/value pairs")),
236                ],
237                vec!["collection", "body"],
238            ),
239        },
240        ToolDef {
241            name: "reddb_kv_get",
242            description: "Get a value by key from a key-value collection.",
243            input_schema: schema(
244                vec![
245                    ("collection", "string", "Collection name"),
246                    ("key", "string", "Key to retrieve"),
247                ],
248                vec!["collection", "key"],
249            ),
250        },
251        ToolDef {
252            name: "reddb_kv_set",
253            description: "Set a key-value pair in a collection.",
254            input_schema: schema_with_nested(
255                vec![
256                    ("collection", string_field("Collection name")),
257                    ("key", string_field("Key to set")),
258                    ("value", {
259                        let mut f = Map::new();
260                        f.insert("description".to_string(), JsonValue::String("Value to store (string, number, boolean, or null)".to_string()));
261                        JsonValue::Object(f)
262                    }),
263                    ("tags", string_array_field("Optional KV invalidation tags")),
264                    ("metadata", object_field("Optional metadata key/value pairs")),
265                ],
266                vec!["collection", "key", "value"],
267            ),
268        },
269        ToolDef {
270            name: "reddb_kv_invalidate_tags",
271            description: "Delete every KV entry in a collection tagged with any listed tag.",
272            input_schema: schema_with_nested(
273                vec![
274                    ("collection", string_field("Collection name")),
275                    ("tags", string_array_field("Tags to invalidate")),
276                ],
277                vec!["collection", "tags"],
278            ),
279        },
280        ToolDef {
281            name: "reddb_config_get",
282            description: "Get a Config value without resolving SecretRef targets.",
283            input_schema: schema(
284                vec![
285                    ("collection", "string", "Config collection name"),
286                    ("key", "string", "Config key to retrieve"),
287                ],
288                vec!["collection", "key"],
289            ),
290        },
291        ToolDef {
292            name: "reddb_config_put",
293            description: "Set a Config value. TTL and counter operations are not supported for Config.",
294            input_schema: schema_with_nested(
295                vec![
296                    ("collection", string_field("Config collection name")),
297                    ("key", string_field("Config key to set")),
298                    ("value", {
299                        let mut f = Map::new();
300                        f.insert("description".to_string(), JsonValue::String("Value to store, or an object when paired with secret_ref".to_string()));
301                        JsonValue::Object(f)
302                    }),
303                    ("secret_ref", object_field("Optional { collection, key } Vault SecretRef")),
304                    ("tags", string_array_field("Optional Config tags")),
305                ],
306                vec!["collection", "key"],
307            ),
308        },
309        ToolDef {
310            name: "reddb_config_resolve",
311            description: "Explicitly resolve a Config SecretRef. Requires the corresponding Vault unseal permission.",
312            input_schema: schema(
313                vec![
314                    ("collection", "string", "Config collection name"),
315                    ("key", "string", "Config key to resolve"),
316                ],
317                vec!["collection", "key"],
318            ),
319        },
320        ToolDef {
321            name: "reddb_vault_get",
322            description: "Get Vault metadata for a secret. Does not return plaintext.",
323            input_schema: schema(
324                vec![
325                    ("collection", "string", "Vault collection name"),
326                    ("key", "string", "Vault key to retrieve metadata for"),
327                ],
328                vec!["collection", "key"],
329            ),
330        },
331        ToolDef {
332            name: "reddb_vault_put",
333            description: "Store a sealed Vault secret. TTL and counter operations are not supported for Vault.",
334            input_schema: schema_with_nested(
335                vec![
336                    ("collection", string_field("Vault collection name")),
337                    ("key", string_field("Vault key to set")),
338                    ("value", {
339                        let mut f = Map::new();
340                        f.insert("description".to_string(), JsonValue::String("Secret value to seal".to_string()));
341                        JsonValue::Object(f)
342                    }),
343                    ("tags", string_array_field("Optional Vault tags")),
344                ],
345                vec!["collection", "key", "value"],
346            ),
347        },
348        ToolDef {
349            name: "reddb_vault_unseal",
350            description: "Explicitly unseal a Vault secret and return plaintext to an authorized caller.",
351            input_schema: schema(
352                vec![
353                    ("collection", "string", "Vault collection name"),
354                    ("key", "string", "Vault key to unseal"),
355                ],
356                vec!["collection", "key"],
357            ),
358        },
359        ToolDef {
360            name: "reddb_delete",
361            description: "Delete an entity by ID from a collection.",
362            input_schema: schema(
363                vec![
364                    ("collection", "string", "Collection name"),
365                    ("id", "integer", "Entity ID to delete"),
366                ],
367                vec!["collection", "id"],
368            ),
369        },
370        ToolDef {
371            name: "reddb_search_vector",
372            description: "Search for similar vectors using cosine similarity.",
373            input_schema: schema_with_nested(
374                vec![
375                    ("collection", string_field("Collection to search in")),
376                    ("vector", number_array_field("Query vector (array of floats)")),
377                    ("k", integer_field("Number of results to return (default 10)")),
378                    ("min_score", number_field("Minimum similarity score threshold (default 0.0)")),
379                ],
380                vec!["collection", "vector"],
381            ),
382        },
383        ToolDef {
384            name: "reddb_search_text",
385            description: "Full-text search across collections.",
386            input_schema: schema_with_nested(
387                vec![
388                    ("query", string_field("Search query string")),
389                    ("collections", {
390                        let mut items = Map::new();
391                        items.insert("type".to_string(), JsonValue::String("string".to_string()));
392                        let mut f = Map::new();
393                        f.insert("type".to_string(), JsonValue::String("array".to_string()));
394                        f.insert("items".to_string(), JsonValue::Object(items));
395                        f.insert("description".to_string(), JsonValue::String("Optional list of collections to search".to_string()));
396                        JsonValue::Object(f)
397                    }),
398                    ("limit", integer_field("Maximum number of results (default 10)")),
399                    ("fuzzy", boolean_field("Enable fuzzy matching (default false)")),
400                ],
401                vec!["query"],
402            ),
403        },
404        ToolDef {
405            name: "reddb_health",
406            description: "Check database health and return runtime statistics.",
407            input_schema: schema(vec![], vec![]),
408        },
409        ToolDef {
410            name: "reddb_graph_traverse",
411            description: "Traverse the graph from a source node using BFS or DFS.",
412            input_schema: schema_with_nested(
413                vec![
414                    ("source", string_field("Source node label to start traversal from")),
415                    ("direction", string_field("Traversal direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
416                    ("max_depth", integer_field("Maximum traversal depth (default 3)")),
417                    ("strategy", string_field("Traversal strategy: 'bfs' or 'dfs' (default 'bfs')")),
418                ],
419                vec!["source"],
420            ),
421        },
422        ToolDef {
423            name: "reddb_graph_shortest_path",
424            description: "Find the shortest path between two graph nodes.",
425            input_schema: schema_with_nested(
426                vec![
427                    ("source", string_field("Source node label")),
428                    ("target", string_field("Target node label")),
429                    ("direction", string_field("Edge direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
430                    (
431                        "algorithm",
432                        string_field(
433                            "Path algorithm: 'bfs', 'dijkstra', 'astar', or 'bellman_ford' (default 'bfs')",
434                        ),
435                    ),
436                ],
437                vec!["source", "target"],
438            ),
439        },
440        // Auth tools
441        ToolDef {
442            name: "reddb_auth_bootstrap",
443            description: "Bootstrap the first admin user. Only works when no users exist yet. Returns the admin user and an API key.",
444            input_schema: schema(
445                vec![
446                    ("username", "string", "Admin username"),
447                    ("password", "string", "Admin password"),
448                ],
449                vec!["username", "password"],
450            ),
451        },
452        ToolDef {
453            name: "reddb_auth_create_user",
454            description: "Create a new database user with a role (admin, write, or read).",
455            input_schema: schema(
456                vec![
457                    ("username", "string", "Username for the new user"),
458                    ("password", "string", "Password for the new user"),
459                    ("role", "string", "Role: 'admin', 'write', or 'read'"),
460                ],
461                vec!["username", "password", "role"],
462            ),
463        },
464        ToolDef {
465            name: "reddb_auth_login",
466            description: "Login with username and password. Returns a session token.",
467            input_schema: schema(
468                vec![
469                    ("username", "string", "Username"),
470                    ("password", "string", "Password"),
471                ],
472                vec!["username", "password"],
473            ),
474        },
475        ToolDef {
476            name: "reddb_auth_create_api_key",
477            description: "Create a persistent API key for a user.",
478            input_schema: schema(
479                vec![
480                    ("username", "string", "Username to create the key for"),
481                    ("name", "string", "Human-readable label for the key"),
482                    ("role", "string", "Role for this key: 'admin', 'write', or 'read'"),
483                ],
484                vec!["username", "name", "role"],
485            ),
486        },
487        ToolDef {
488            name: "reddb_auth_list_users",
489            description: "List all database users and their roles.",
490            input_schema: schema(vec![], vec![]),
491        },
492        // Update / Scan tools
493        ToolDef {
494            name: "reddb_update",
495            description: "Update entities in a collection matching a filter.",
496            input_schema: schema_with_nested(
497                vec![
498                    ("collection", string_field("Collection name")),
499                    ("set", object_field("Key-value pairs to update")),
500                    (
501                        "where_filter",
502                        string_field(
503                            "Optional SQL WHERE clause (e.g., \"age > 21\")",
504                        ),
505                    ),
506                ],
507                vec!["collection", "set"],
508            ),
509        },
510        ToolDef {
511            name: "reddb_scan",
512            description: "Scan entities from a collection with pagination.",
513            input_schema: schema_with_nested(
514                vec![
515                    ("collection", string_field("Collection to scan")),
516                    ("limit", integer_field("Maximum number of results (default 10)")),
517                    ("offset", integer_field("Number of records to skip (default 0)")),
518                ],
519                vec!["collection"],
520            ),
521        },
522        // Graph analytics tools
523        ToolDef {
524            name: "reddb_graph_centrality",
525            description: "Compute centrality scores for graph nodes. Algorithms: degree, closeness, betweenness, eigenvector, pagerank.",
526            input_schema: schema_with_nested(
527                vec![(
528                    "algorithm",
529                    string_field(
530                        "Centrality algorithm: 'degree', 'closeness', 'betweenness', 'eigenvector', 'pagerank'",
531                    ),
532                )],
533                vec!["algorithm"],
534            ),
535        },
536        ToolDef {
537            name: "reddb_graph_community",
538            description: "Detect communities in the graph. Algorithms: label_propagation, louvain.",
539            input_schema: schema_with_nested(
540                vec![
541                    (
542                        "algorithm",
543                        string_field(
544                            "Community detection algorithm: 'label_propagation' or 'louvain'",
545                        ),
546                    ),
547                    (
548                        "max_iterations",
549                        integer_field("Maximum iterations (default 100)"),
550                    ),
551                ],
552                vec!["algorithm"],
553            ),
554        },
555        ToolDef {
556            name: "reddb_graph_components",
557            description: "Find connected components in the graph.",
558            input_schema: schema_with_nested(
559                vec![(
560                    "mode",
561                    string_field(
562                        "Component mode: 'weakly_connected' or 'strongly_connected' (default 'weakly_connected')",
563                    ),
564                )],
565                vec![],
566            ),
567        },
568        ToolDef {
569            name: "reddb_graph_cycles",
570            description: "Detect cycles in the graph.",
571            input_schema: schema_with_nested(
572                vec![
573                    (
574                        "max_length",
575                        integer_field("Maximum cycle length (default 10)"),
576                    ),
577                    (
578                        "max_cycles",
579                        integer_field("Maximum number of cycles to return (default 100)"),
580                    ),
581                ],
582                vec![],
583            ),
584        },
585        ToolDef {
586            name: "reddb_graph_clustering",
587            description: "Compute clustering coefficient for the graph.",
588            input_schema: schema(vec![], vec![]),
589        },
590        // DDL tools
591        ToolDef {
592            name: "reddb_create_collection",
593            description: "Create a new collection (table) in the database.",
594            input_schema: schema(
595                vec![("name", "string", "Collection name to create")],
596                vec!["name"],
597            ),
598        },
599        ToolDef {
600            name: "reddb_drop_collection",
601            description: "Drop (delete) a collection from the database.",
602            input_schema: schema(
603                vec![("name", "string", "Collection name to drop")],
604                vec!["name"],
605            ),
606        },
607    ]
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn test_all_tools_defined() {
616        let tools = all_tools();
617        assert!(tools.len() >= 24);
618        let names: Vec<&str> = tools.iter().map(|t| t.name).collect();
619        assert!(names.contains(&"reddb_query"));
620        assert!(names.contains(&"reddb_collections"));
621        assert!(names.contains(&"reddb_insert_row"));
622        assert!(names.contains(&"reddb_insert_node"));
623        assert!(names.contains(&"reddb_insert_edge"));
624        assert!(names.contains(&"reddb_insert_vector"));
625        assert!(names.contains(&"reddb_insert_document"));
626        assert!(names.contains(&"reddb_kv_get"));
627        assert!(names.contains(&"reddb_kv_set"));
628        assert!(names.contains(&"reddb_config_get"));
629        assert!(names.contains(&"reddb_config_put"));
630        assert!(names.contains(&"reddb_config_resolve"));
631        assert!(names.contains(&"reddb_vault_get"));
632        assert!(names.contains(&"reddb_vault_put"));
633        assert!(names.contains(&"reddb_vault_unseal"));
634        assert!(names.contains(&"reddb_delete"));
635        assert!(names.contains(&"reddb_search_vector"));
636        assert!(names.contains(&"reddb_search_text"));
637        assert!(names.contains(&"reddb_health"));
638        assert!(names.contains(&"reddb_graph_traverse"));
639        assert!(names.contains(&"reddb_graph_shortest_path"));
640        // New tools
641        assert!(names.contains(&"reddb_update"));
642        assert!(names.contains(&"reddb_scan"));
643        assert!(names.contains(&"reddb_graph_centrality"));
644        assert!(names.contains(&"reddb_graph_community"));
645        assert!(names.contains(&"reddb_graph_components"));
646        assert!(names.contains(&"reddb_graph_cycles"));
647        assert!(names.contains(&"reddb_graph_clustering"));
648        assert!(names.contains(&"reddb_create_collection"));
649        assert!(names.contains(&"reddb_drop_collection"));
650    }
651
652    #[test]
653    fn test_tool_schemas_have_type() {
654        for tool in all_tools() {
655            assert_eq!(
656                tool.input_schema.get("type").and_then(|v| v.as_str()),
657                Some("object"),
658                "tool '{}' schema must have type=object",
659                tool.name,
660            );
661        }
662    }
663
664    #[test]
665    fn test_update_tool_schema() {
666        let tools = all_tools();
667        let tool = tools.iter().find(|t| t.name == "reddb_update").unwrap();
668        assert_eq!(tool.name, "reddb_update");
669        let props = tool
670            .input_schema
671            .get("properties")
672            .and_then(|v| v.as_object())
673            .unwrap();
674        assert!(props.contains_key("collection"));
675        assert!(props.contains_key("set"));
676        assert!(props.contains_key("where_filter"));
677        let required = tool
678            .input_schema
679            .get("required")
680            .and_then(|v| v.as_array())
681            .unwrap();
682        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
683        assert!(required_strs.contains(&"collection"));
684        assert!(required_strs.contains(&"set"));
685        assert!(!required_strs.contains(&"where_filter"));
686    }
687
688    #[test]
689    fn test_scan_tool_schema() {
690        let tools = all_tools();
691        let tool = tools.iter().find(|t| t.name == "reddb_scan").unwrap();
692        let props = tool
693            .input_schema
694            .get("properties")
695            .and_then(|v| v.as_object())
696            .unwrap();
697        assert!(props.contains_key("collection"));
698        assert!(props.contains_key("limit"));
699        assert!(props.contains_key("offset"));
700        let required = tool
701            .input_schema
702            .get("required")
703            .and_then(|v| v.as_array())
704            .unwrap();
705        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
706        assert!(required_strs.contains(&"collection"));
707        // limit and offset are optional
708        assert!(!required_strs.contains(&"limit"));
709        assert!(!required_strs.contains(&"offset"));
710    }
711
712    #[test]
713    fn test_graph_centrality_tool_schema() {
714        let tools = all_tools();
715        let tool = tools
716            .iter()
717            .find(|t| t.name == "reddb_graph_centrality")
718            .unwrap();
719        let props = tool
720            .input_schema
721            .get("properties")
722            .and_then(|v| v.as_object())
723            .unwrap();
724        assert!(props.contains_key("algorithm"));
725        let required = tool
726            .input_schema
727            .get("required")
728            .and_then(|v| v.as_array())
729            .unwrap();
730        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
731        assert!(required_strs.contains(&"algorithm"));
732    }
733
734    #[test]
735    fn test_graph_community_tool_schema() {
736        let tools = all_tools();
737        let tool = tools
738            .iter()
739            .find(|t| t.name == "reddb_graph_community")
740            .unwrap();
741        let props = tool
742            .input_schema
743            .get("properties")
744            .and_then(|v| v.as_object())
745            .unwrap();
746        assert!(props.contains_key("algorithm"));
747        assert!(props.contains_key("max_iterations"));
748        let required = tool
749            .input_schema
750            .get("required")
751            .and_then(|v| v.as_array())
752            .unwrap();
753        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
754        assert!(required_strs.contains(&"algorithm"));
755        assert!(!required_strs.contains(&"max_iterations"));
756    }
757
758    #[test]
759    fn test_graph_components_tool_schema() {
760        let tools = all_tools();
761        let tool = tools
762            .iter()
763            .find(|t| t.name == "reddb_graph_components")
764            .unwrap();
765        let props = tool
766            .input_schema
767            .get("properties")
768            .and_then(|v| v.as_object())
769            .unwrap();
770        assert!(props.contains_key("mode"));
771        let required = tool
772            .input_schema
773            .get("required")
774            .and_then(|v| v.as_array())
775            .unwrap();
776        // mode is optional (has default)
777        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
778        assert!(required_strs.is_empty());
779    }
780
781    #[test]
782    fn test_graph_cycles_tool_schema() {
783        let tools = all_tools();
784        let tool = tools
785            .iter()
786            .find(|t| t.name == "reddb_graph_cycles")
787            .unwrap();
788        let props = tool
789            .input_schema
790            .get("properties")
791            .and_then(|v| v.as_object())
792            .unwrap();
793        assert!(props.contains_key("max_length"));
794        assert!(props.contains_key("max_cycles"));
795        let required = tool
796            .input_schema
797            .get("required")
798            .and_then(|v| v.as_array())
799            .unwrap();
800        // All optional
801        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
802        assert!(required_strs.is_empty());
803    }
804
805    #[test]
806    fn test_graph_clustering_tool_schema() {
807        let tools = all_tools();
808        let tool = tools
809            .iter()
810            .find(|t| t.name == "reddb_graph_clustering")
811            .unwrap();
812        let props = tool
813            .input_schema
814            .get("properties")
815            .and_then(|v| v.as_object())
816            .unwrap();
817        // No required properties - takes no arguments
818        assert!(props.is_empty());
819    }
820
821    #[test]
822    fn test_create_collection_tool_schema() {
823        let tools = all_tools();
824        let tool = tools
825            .iter()
826            .find(|t| t.name == "reddb_create_collection")
827            .unwrap();
828        let props = tool
829            .input_schema
830            .get("properties")
831            .and_then(|v| v.as_object())
832            .unwrap();
833        assert!(props.contains_key("name"));
834        let name_type = props
835            .get("name")
836            .and_then(|v| v.get("type"))
837            .and_then(|v| v.as_str());
838        assert_eq!(name_type, Some("string"));
839        let required = tool
840            .input_schema
841            .get("required")
842            .and_then(|v| v.as_array())
843            .unwrap();
844        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
845        assert!(required_strs.contains(&"name"));
846    }
847
848    #[test]
849    fn test_drop_collection_tool_schema() {
850        let tools = all_tools();
851        let tool = tools
852            .iter()
853            .find(|t| t.name == "reddb_drop_collection")
854            .unwrap();
855        let props = tool
856            .input_schema
857            .get("properties")
858            .and_then(|v| v.as_object())
859            .unwrap();
860        assert!(props.contains_key("name"));
861        let required = tool
862            .input_schema
863            .get("required")
864            .and_then(|v| v.as_array())
865            .unwrap();
866        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
867        assert!(required_strs.contains(&"name"));
868    }
869}