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