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/// Definition of a read-only MCP knowledge resource (ADR 0061).
16///
17/// The body is produced lazily by `body()` from the engine's own authorities
18/// (e.g. `reddb_rql::knowledge` reads the lexer keyword set and the
19/// `reddb-io-types` function catalog), so the served text cannot drift from the
20/// engine and the resource carries no mutable state.
21pub struct ResourceDef {
22    pub uri: &'static str,
23    pub title: &'static str,
24    pub description: &'static str,
25    pub mime_type: &'static str,
26    pub body: fn() -> String,
27}
28
29/// The static set of knowledge resources `red mcp` serves. Each domain is
30/// generated from the engine's own authorities (ADR 0061): the RQL reference
31/// from `reddb-io-rql`, and the value-type catalog + multi-model map from
32/// `reddb-io-types`, and the connection model from `reddb-io-wire`.
33pub fn knowledge_resources() -> Vec<ResourceDef> {
34    vec![
35        ResourceDef {
36            uri: reddb_rql::knowledge::RESOURCE_URI,
37            title: reddb_rql::knowledge::RESOURCE_TITLE,
38            description: reddb_rql::knowledge::RESOURCE_DESCRIPTION,
39            mime_type: "text/markdown",
40            body: reddb_rql::knowledge::rql_reference_markdown,
41        },
42        ResourceDef {
43            uri: reddb_types::knowledge::RESOURCE_URI,
44            title: reddb_types::knowledge::RESOURCE_TITLE,
45            description: reddb_types::knowledge::RESOURCE_DESCRIPTION,
46            mime_type: "text/markdown",
47            body: reddb_types::knowledge::type_reference_markdown,
48        },
49        ResourceDef {
50            uri: reddb_wire::knowledge::RESOURCE_URI,
51            title: reddb_wire::knowledge::RESOURCE_TITLE,
52            description: reddb_wire::knowledge::RESOURCE_DESCRIPTION,
53            mime_type: "text/markdown",
54            body: reddb_wire::knowledge::connection_reference_markdown,
55        },
56    ]
57}
58
59/// Build a JSON Schema object from a list of field descriptors.
60fn schema(properties: Vec<(&str, &str, &str)>, required: Vec<&str>) -> JsonValue {
61    let mut props = Map::new();
62    for (name, field_type, description) in properties {
63        let mut field = Map::new();
64        field.insert(
65            "type".to_string(),
66            JsonValue::String(field_type.to_string()),
67        );
68        if !description.is_empty() {
69            field.insert(
70                "description".to_string(),
71                JsonValue::String(description.to_string()),
72            );
73        }
74        props.insert(name.to_string(), JsonValue::Object(field));
75    }
76
77    let mut obj = Map::new();
78    obj.insert("type".to_string(), JsonValue::String("object".to_string()));
79    obj.insert("properties".to_string(), JsonValue::Object(props));
80    obj.insert(
81        "required".to_string(),
82        JsonValue::Array(
83            required
84                .into_iter()
85                .map(|s| JsonValue::String(s.to_string()))
86                .collect(),
87        ),
88    );
89    obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
90    JsonValue::Object(obj)
91}
92
93/// Build a JSON Schema object that accepts items with flexible inner types.
94fn schema_with_nested(properties: Vec<(&str, JsonValue)>, required: Vec<&str>) -> JsonValue {
95    let mut props = Map::new();
96    for (name, descriptor) in properties {
97        props.insert(name.to_string(), descriptor);
98    }
99
100    let mut obj = Map::new();
101    obj.insert("type".to_string(), JsonValue::String("object".to_string()));
102    obj.insert("properties".to_string(), JsonValue::Object(props));
103    obj.insert(
104        "required".to_string(),
105        JsonValue::Array(
106            required
107                .into_iter()
108                .map(|s| JsonValue::String(s.to_string()))
109                .collect(),
110        ),
111    );
112    obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
113    JsonValue::Object(obj)
114}
115
116/// Simple string field descriptor.
117fn string_field(description: &str) -> JsonValue {
118    let mut f = Map::new();
119    f.insert("type".to_string(), JsonValue::String("string".to_string()));
120    f.insert(
121        "description".to_string(),
122        JsonValue::String(description.to_string()),
123    );
124    JsonValue::Object(f)
125}
126
127/// Simple number field descriptor.
128fn number_field(description: &str) -> JsonValue {
129    let mut f = Map::new();
130    f.insert("type".to_string(), JsonValue::String("number".to_string()));
131    f.insert(
132        "description".to_string(),
133        JsonValue::String(description.to_string()),
134    );
135    JsonValue::Object(f)
136}
137
138/// Simple integer field descriptor.
139fn integer_field(description: &str) -> JsonValue {
140    let mut f = Map::new();
141    f.insert("type".to_string(), JsonValue::String("integer".to_string()));
142    f.insert(
143        "description".to_string(),
144        JsonValue::String(description.to_string()),
145    );
146    JsonValue::Object(f)
147}
148
149/// Simple boolean field descriptor.
150fn boolean_field(description: &str) -> JsonValue {
151    let mut f = Map::new();
152    f.insert("type".to_string(), JsonValue::String("boolean".to_string()));
153    f.insert(
154        "description".to_string(),
155        JsonValue::String(description.to_string()),
156    );
157    JsonValue::Object(f)
158}
159
160fn type_field(field_type: &str) -> JsonValue {
161    let mut f = Map::new();
162    f.insert(
163        "type".to_string(),
164        JsonValue::String(field_type.to_string()),
165    );
166    JsonValue::Object(f)
167}
168
169/// Object field descriptor (accepts arbitrary JSON object).
170fn object_field(description: &str) -> JsonValue {
171    let mut f = Map::new();
172    f.insert("type".to_string(), JsonValue::String("object".to_string()));
173    f.insert(
174        "description".to_string(),
175        JsonValue::String(description.to_string()),
176    );
177    JsonValue::Object(f)
178}
179
180/// Array-of-numbers field descriptor.
181fn number_array_field(description: &str) -> JsonValue {
182    let mut items = Map::new();
183    items.insert("type".to_string(), JsonValue::String("number".to_string()));
184
185    let mut f = Map::new();
186    f.insert("type".to_string(), JsonValue::String("array".to_string()));
187    f.insert("items".to_string(), JsonValue::Object(items));
188    f.insert(
189        "description".to_string(),
190        JsonValue::String(description.to_string()),
191    );
192    JsonValue::Object(f)
193}
194
195/// Array-of-strings field descriptor.
196fn string_array_field(description: &str) -> JsonValue {
197    let mut items = Map::new();
198    items.insert("type".to_string(), JsonValue::String("string".to_string()));
199
200    let mut f = Map::new();
201    f.insert("type".to_string(), JsonValue::String("array".to_string()));
202    f.insert("items".to_string(), JsonValue::Object(items));
203    f.insert(
204        "description".to_string(),
205        JsonValue::String(description.to_string()),
206    );
207    JsonValue::Object(f)
208}
209
210/// Return all tool definitions exposed by the RedDB MCP server.
211pub fn all_tools() -> Vec<ToolDef> {
212    vec![
213        ToolDef {
214            name: "reddb_query",
215            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.",
216            input_schema: schema_with_nested(
217                vec![
218                    ("sql", string_field("SQL or universal query to execute. Use `$1`, `$2`, ... placeholders for any value that came from the user.")),
219                    ("params", {
220                        let mut items = Map::new();
221                        items.insert("description".to_string(), JsonValue::String("Bind value for the matching $N placeholder. Accepts null, boolean, number, string, array, or object.".to_string()));
222                        items.insert(
223                            "anyOf".to_string(),
224                            JsonValue::Array(vec![
225                                type_field("null"),
226                                type_field("boolean"),
227                                type_field("number"),
228                                type_field("string"),
229                                type_field("array"),
230                                type_field("object"),
231                            ]),
232                        );
233                        let mut f = Map::new();
234                        f.insert("type".to_string(), JsonValue::String("array".to_string()));
235                        f.insert("items".to_string(), JsonValue::Object(items));
236                        f.insert(
237                            "description".to_string(),
238                            JsonValue::String(
239                                "Positional bind values for `$1`, `$2`, ... in `sql`. Index 0 binds `$1`."
240                                    .to_string(),
241                            ),
242                        );
243                        JsonValue::Object(f)
244                    }),
245                ],
246                vec!["sql"],
247            ),
248        },
249        ToolDef {
250            name: "reddb_collections",
251            description: "List all collections in the database.",
252            input_schema: schema(vec![], vec![]),
253        },
254        ToolDef {
255            name: "reddb_insert_row",
256            description: "Insert a table row into a collection.",
257            input_schema: schema_with_nested(
258                vec![
259                    ("collection", string_field("Target collection name")),
260                    ("data", object_field("Object with field name/value pairs to insert")),
261                    ("metadata", object_field("Optional metadata key/value pairs")),
262                ],
263                vec!["collection", "data"],
264            ),
265        },
266        ToolDef {
267            name: "reddb_insert_node",
268            description: "Insert a graph node into a collection.",
269            input_schema: schema_with_nested(
270                vec![
271                    ("collection", string_field("Target collection name")),
272                    ("label", string_field("Node label (identifier)")),
273                    ("node_type", string_field("Optional node type classification")),
274                    ("properties", object_field("Optional node properties as key/value pairs")),
275                    ("metadata", object_field("Optional metadata key/value pairs")),
276                ],
277                vec!["collection", "label"],
278            ),
279        },
280        ToolDef {
281            name: "reddb_insert_edge",
282            description: "Insert a graph edge between two nodes.",
283            input_schema: schema_with_nested(
284                vec![
285                    ("collection", string_field("Target collection name")),
286                    ("label", string_field("Edge label (relationship type)")),
287                    ("from", integer_field("Source node entity ID")),
288                    ("to", integer_field("Target node entity ID")),
289                    ("weight", number_field("Optional edge weight (default 1.0)")),
290                    ("properties", object_field("Optional edge properties")),
291                    ("metadata", object_field("Optional metadata key/value pairs")),
292                ],
293                vec!["collection", "label", "from", "to"],
294            ),
295        },
296        ToolDef {
297            name: "reddb_insert_vector",
298            description: "Insert a vector embedding into a collection.",
299            input_schema: schema_with_nested(
300                vec![
301                    ("collection", string_field("Target collection name")),
302                    ("dense", number_array_field("Dense vector (array of floats)")),
303                    ("content", string_field("Optional text content associated with the vector")),
304                    ("metadata", object_field("Optional metadata key/value pairs")),
305                ],
306                vec!["collection", "dense"],
307            ),
308        },
309        ToolDef {
310            name: "reddb_insert_document",
311            description: "Insert a JSON document into a collection.",
312            input_schema: schema_with_nested(
313                vec![
314                    ("collection", string_field("Target collection name")),
315                    ("body", object_field("JSON document body")),
316                    ("metadata", object_field("Optional metadata key/value pairs")),
317                ],
318                vec!["collection", "body"],
319            ),
320        },
321        ToolDef {
322            name: "reddb_kv_get",
323            description: "Get a value by key from a key-value collection.",
324            input_schema: schema(
325                vec![
326                    ("collection", "string", "Collection name"),
327                    ("key", "string", "Key to retrieve"),
328                ],
329                vec!["collection", "key"],
330            ),
331        },
332        ToolDef {
333            name: "reddb_kv_set",
334            description: "Set a key-value pair in a collection.",
335            input_schema: schema_with_nested(
336                vec![
337                    ("collection", string_field("Collection name")),
338                    ("key", string_field("Key to set")),
339                    ("value", {
340                        let mut f = Map::new();
341                        f.insert("description".to_string(), JsonValue::String("Value to store (string, number, boolean, or null)".to_string()));
342                        JsonValue::Object(f)
343                    }),
344                    ("tags", string_array_field("Optional KV invalidation tags")),
345                    ("metadata", object_field("Optional metadata key/value pairs")),
346                ],
347                vec!["collection", "key", "value"],
348            ),
349        },
350        ToolDef {
351            name: "reddb_kv_invalidate_tags",
352            description: "Delete every KV entry in a collection tagged with any listed tag.",
353            input_schema: schema_with_nested(
354                vec![
355                    ("collection", string_field("Collection name")),
356                    ("tags", string_array_field("Tags to invalidate")),
357                ],
358                vec!["collection", "tags"],
359            ),
360        },
361        ToolDef {
362            name: "reddb_config_get",
363            description: "Get a Config value without resolving SecretRef targets.",
364            input_schema: schema(
365                vec![
366                    ("collection", "string", "Config collection name"),
367                    ("key", "string", "Config key to retrieve"),
368                ],
369                vec!["collection", "key"],
370            ),
371        },
372        ToolDef {
373            name: "reddb_config_put",
374            description: "Set a Config value. TTL and counter operations are not supported for Config.",
375            input_schema: schema_with_nested(
376                vec![
377                    ("collection", string_field("Config collection name")),
378                    ("key", string_field("Config key to set")),
379                    ("value", {
380                        let mut f = Map::new();
381                        f.insert("description".to_string(), JsonValue::String("Value to store, or an object when paired with secret_ref".to_string()));
382                        JsonValue::Object(f)
383                    }),
384                    ("secret_ref", object_field("Optional { collection, key } Vault SecretRef")),
385                    ("tags", string_array_field("Optional Config tags")),
386                ],
387                vec!["collection", "key"],
388            ),
389        },
390        ToolDef {
391            name: "reddb_config_resolve",
392            description: "Explicitly resolve a Config SecretRef. Requires the corresponding Vault unseal permission.",
393            input_schema: schema(
394                vec![
395                    ("collection", "string", "Config collection name"),
396                    ("key", "string", "Config key to resolve"),
397                ],
398                vec!["collection", "key"],
399            ),
400        },
401        ToolDef {
402            name: "reddb_vault_get",
403            description: "Get Vault metadata for a secret. Does not return plaintext.",
404            input_schema: schema(
405                vec![
406                    ("collection", "string", "Vault collection name"),
407                    ("key", "string", "Vault key to retrieve metadata for"),
408                ],
409                vec!["collection", "key"],
410            ),
411        },
412        ToolDef {
413            name: "reddb_vault_put",
414            description: "Store a sealed Vault secret. TTL and counter operations are not supported for Vault.",
415            input_schema: schema_with_nested(
416                vec![
417                    ("collection", string_field("Vault collection name")),
418                    ("key", string_field("Vault key to set")),
419                    ("value", {
420                        let mut f = Map::new();
421                        f.insert("description".to_string(), JsonValue::String("Secret value to seal".to_string()));
422                        JsonValue::Object(f)
423                    }),
424                    ("tags", string_array_field("Optional Vault tags")),
425                ],
426                vec!["collection", "key", "value"],
427            ),
428        },
429        ToolDef {
430            name: "reddb_vault_unseal",
431            description: "Explicitly unseal a Vault secret and return plaintext to an authorized caller.",
432            input_schema: schema(
433                vec![
434                    ("collection", "string", "Vault collection name"),
435                    ("key", "string", "Vault key to unseal"),
436                ],
437                vec!["collection", "key"],
438            ),
439        },
440        ToolDef {
441            name: "reddb_delete",
442            description: "Delete an entity by ID from a collection.",
443            input_schema: schema(
444                vec![
445                    ("collection", "string", "Collection name"),
446                    ("id", "integer", "Entity ID to delete"),
447                ],
448                vec!["collection", "id"],
449            ),
450        },
451        ToolDef {
452            name: "reddb_search_vector",
453            description: "Search for similar vectors using cosine similarity.",
454            input_schema: schema_with_nested(
455                vec![
456                    ("collection", string_field("Collection to search in")),
457                    ("vector", number_array_field("Query vector (array of floats)")),
458                    ("k", integer_field("Number of results to return (default 10)")),
459                    ("min_score", number_field("Minimum similarity score threshold (default 0.0)")),
460                ],
461                vec!["collection", "vector"],
462            ),
463        },
464        ToolDef {
465            name: "reddb_search_text",
466            description: "Full-text search across collections.",
467            input_schema: schema_with_nested(
468                vec![
469                    ("query", string_field("Search query string")),
470                    ("collections", {
471                        let mut items = Map::new();
472                        items.insert("type".to_string(), JsonValue::String("string".to_string()));
473                        let mut f = Map::new();
474                        f.insert("type".to_string(), JsonValue::String("array".to_string()));
475                        f.insert("items".to_string(), JsonValue::Object(items));
476                        f.insert("description".to_string(), JsonValue::String("Optional list of collections to search".to_string()));
477                        JsonValue::Object(f)
478                    }),
479                    ("limit", integer_field("Maximum number of results (default 10)")),
480                    ("fuzzy", boolean_field("Enable fuzzy matching (default false)")),
481                ],
482                vec!["query"],
483            ),
484        },
485        ToolDef {
486            name: "reddb_health",
487            description: "Check database health and return runtime statistics.",
488            input_schema: schema(vec![], vec![]),
489        },
490        ToolDef {
491            name: "reddb_graph_traverse",
492            description: "Traverse the graph from a source node using BFS or DFS.",
493            input_schema: schema_with_nested(
494                vec![
495                    ("source", string_field("Source node label to start traversal from")),
496                    ("direction", string_field("Traversal direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
497                    ("max_depth", integer_field("Maximum traversal depth (default 3)")),
498                    ("strategy", string_field("Traversal strategy: 'bfs' or 'dfs' (default 'bfs')")),
499                ],
500                vec!["source"],
501            ),
502        },
503        ToolDef {
504            name: "reddb_graph_shortest_path",
505            description: "Find the shortest path between two graph nodes.",
506            input_schema: schema_with_nested(
507                vec![
508                    ("source", string_field("Source node label")),
509                    ("target", string_field("Target node label")),
510                    ("direction", string_field("Edge direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
511                    (
512                        "algorithm",
513                        string_field(
514                            "Path algorithm: 'bfs', 'dijkstra', 'astar', or 'bellman_ford' (default 'bfs')",
515                        ),
516                    ),
517                ],
518                vec!["source", "target"],
519            ),
520        },
521        // Auth tools
522        ToolDef {
523            name: "reddb_auth_bootstrap",
524            description: "Bootstrap the first admin user. Only works when no users exist yet. Returns the admin user and an API key.",
525            input_schema: schema(
526                vec![
527                    ("username", "string", "Admin username"),
528                    ("password", "string", "Admin password"),
529                ],
530                vec!["username", "password"],
531            ),
532        },
533        ToolDef {
534            name: "reddb_auth_create_user",
535            description: "Create a new database user with a role (admin, write, or read).",
536            input_schema: schema(
537                vec![
538                    ("username", "string", "Username for the new user"),
539                    ("password", "string", "Password for the new user"),
540                    ("role", "string", "Role: 'admin', 'write', or 'read'"),
541                ],
542                vec!["username", "password", "role"],
543            ),
544        },
545        ToolDef {
546            name: "reddb_auth_login",
547            description: "Login with username and password. Returns a session token.",
548            input_schema: schema(
549                vec![
550                    ("username", "string", "Username"),
551                    ("password", "string", "Password"),
552                ],
553                vec!["username", "password"],
554            ),
555        },
556        ToolDef {
557            name: "reddb_auth_create_api_key",
558            description: "Create a persistent API key for a user.",
559            input_schema: schema(
560                vec![
561                    ("username", "string", "Username to create the key for"),
562                    ("name", "string", "Human-readable label for the key"),
563                    ("role", "string", "Role for this key: 'admin', 'write', or 'read'"),
564                ],
565                vec!["username", "name", "role"],
566            ),
567        },
568        ToolDef {
569            name: "reddb_auth_list_users",
570            description: "List all database users and their roles.",
571            input_schema: schema(vec![], vec![]),
572        },
573        // Update / Scan tools
574        ToolDef {
575            name: "reddb_update",
576            description: "Update entities in a collection matching a filter.",
577            input_schema: schema_with_nested(
578                vec![
579                    ("collection", string_field("Collection name")),
580                    ("set", object_field("Key-value pairs to update")),
581                    (
582                        "where_filter",
583                        string_field(
584                            "Optional SQL WHERE clause (e.g., \"age > 21\")",
585                        ),
586                    ),
587                ],
588                vec!["collection", "set"],
589            ),
590        },
591        ToolDef {
592            name: "reddb_scan",
593            description: "Scan entities from a collection with pagination.",
594            input_schema: schema_with_nested(
595                vec![
596                    ("collection", string_field("Collection to scan")),
597                    ("limit", integer_field("Maximum number of results (default 10)")),
598                    ("offset", integer_field("Number of records to skip (default 0)")),
599                ],
600                vec!["collection"],
601            ),
602        },
603        // Graph analytics tools
604        ToolDef {
605            name: "reddb_graph_centrality",
606            description: "Compute centrality scores for graph nodes. Algorithms: degree, closeness, betweenness, eigenvector, pagerank.",
607            input_schema: schema_with_nested(
608                vec![(
609                    "algorithm",
610                    string_field(
611                        "Centrality algorithm: 'degree', 'closeness', 'betweenness', 'eigenvector', 'pagerank'",
612                    ),
613                )],
614                vec!["algorithm"],
615            ),
616        },
617        ToolDef {
618            name: "reddb_graph_community",
619            description: "Detect communities in the graph. Algorithms: label_propagation, louvain.",
620            input_schema: schema_with_nested(
621                vec![
622                    (
623                        "algorithm",
624                        string_field(
625                            "Community detection algorithm: 'label_propagation' or 'louvain'",
626                        ),
627                    ),
628                    (
629                        "max_iterations",
630                        integer_field("Maximum iterations (default 100)"),
631                    ),
632                ],
633                vec!["algorithm"],
634            ),
635        },
636        ToolDef {
637            name: "reddb_graph_components",
638            description: "Find connected components in the graph.",
639            input_schema: schema_with_nested(
640                vec![(
641                    "mode",
642                    string_field(
643                        "Component mode: 'weakly_connected' or 'strongly_connected' (default 'weakly_connected')",
644                    ),
645                )],
646                vec![],
647            ),
648        },
649        ToolDef {
650            name: "reddb_graph_cycles",
651            description: "Detect cycles in the graph.",
652            input_schema: schema_with_nested(
653                vec![
654                    (
655                        "max_length",
656                        integer_field("Maximum cycle length (default 10)"),
657                    ),
658                    (
659                        "max_cycles",
660                        integer_field("Maximum number of cycles to return (default 100)"),
661                    ),
662                ],
663                vec![],
664            ),
665        },
666        ToolDef {
667            name: "reddb_graph_clustering",
668            description: "Compute clustering coefficient for the graph.",
669            input_schema: schema(vec![], vec![]),
670        },
671        // DDL tools
672        ToolDef {
673            name: "reddb_create_collection",
674            description: "Create a new collection (table) in the database.",
675            input_schema: schema(
676                vec![("name", "string", "Collection name to create")],
677                vec!["name"],
678            ),
679        },
680        ToolDef {
681            name: "reddb_drop_collection",
682            description: "Drop (delete) a collection from the database.",
683            input_schema: schema(
684                vec![("name", "string", "Collection name to drop")],
685                vec!["name"],
686            ),
687        },
688        // RQL knowledge tools (ADR 0061): parse a submitted query through the
689        // real `reddb-io-rql` parser so an agent learns the dialect by
690        // submitting, not by guessing.
691        ToolDef {
692            name: "reddb_rql_validate",
693            description: "Validate an RQL (RedDB Query Language) string by parsing it with the real reddb-io-rql parser. Returns `{ valid: true, statement }` with the statement kind on success, or `{ valid: false, error }` with a structured diagnostic (message, line, column, offset, kind, expected) on failure. Submit a query to learn the dialect instead of guessing.",
694            input_schema: schema(
695                vec![("rql", "string", "RQL query string to validate")],
696                vec!["rql"],
697            ),
698        },
699        ToolDef {
700            name: "reddb_rql_explain",
701            description: "Explain an RQL (RedDB Query Language) string: parse it with the real reddb-io-rql parser and, when valid, also return its AST and the optimizer plan (`ast`, `optimized_ast`, `applied_passes`). Invalid input returns the same structured diagnostic as reddb_rql_validate with no AST/plan.",
702            input_schema: schema(
703                vec![("rql", "string", "RQL query string to parse and explain")],
704                vec!["rql"],
705            ),
706        },
707        // Type knowledge tool (ADR 0061): answer a type/cast lookup from the
708        // generated reddb-io-types catalog.
709        ToolDef {
710            name: "reddb_type_of",
711            description: "Look up a RedDB value type from the generated `reddb-io-types` catalog. Given either a type name (`type`, e.g. \"INTEGER\" or the SQL alias \"int\") or a literal JSON value (`value`, e.g. 42 or true), returns the canonical type, its coercion category, the casts available out of it, and the operator overloads that accept it. Provide exactly one of `type` or `value`; `type` wins if both are given.\n\nThe answer is read live from the engine's function/operator/cast catalogs — it never drifts from the type system.",
712            input_schema: schema_with_nested(
713                vec![
714                    ("type", string_field("Type name to look up: a canonical RedDB type (e.g. \"INTEGER\", \"TEXT\") or a common SQL alias (e.g. \"int\", \"string\"), case-insensitive.")),
715                    ("value", {
716                        let mut f = Map::new();
717                        f.insert("description".to_string(), JsonValue::String("A literal JSON value (null, boolean, number, string, array, or object) whose canonical type to look up. Used only when `type` is omitted.".to_string()));
718                        f.insert(
719                            "anyOf".to_string(),
720                            JsonValue::Array(vec![
721                                type_field("null"),
722                                type_field("boolean"),
723                                type_field("number"),
724                                type_field("string"),
725                                type_field("array"),
726                                type_field("object"),
727                            ]),
728                        );
729                        JsonValue::Object(f)
730                    }),
731                ],
732                vec![],
733            ),
734        },
735        // Connection knowledge tool (ADR 0061): project the canonical
736        // reddb-wire connection-string parser into agent-readable transport
737        // and topology facts.
738        ToolDef {
739            name: "reddb_explain_connection",
740            description: "Decode a RedDB connection URI with the canonical `reddb-io-wire` parser and return its scheme, transport, mode (`embedded` or `remote`), topology, and normalized target details. Use this to learn the scheme dispatch: `memory://` is embedded ephemeral, `file://` is embedded persisted, `red://` / `reds://` are remote RedWire, and `grpc://` / `http(s)://` are remote non-RedWire transports.",
741            input_schema: schema(
742                vec![(
743                    "uri",
744                    "string",
745                    "Connection URI to explain, e.g. memory://, file:///data.rdb, reds://host, grpc://host, or https://host",
746                )],
747                vec!["uri"],
748            ),
749        },
750    ]
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn test_all_tools_defined() {
759        let tools = all_tools();
760        assert!(tools.len() >= 24);
761        let names: Vec<&str> = tools.iter().map(|t| t.name).collect();
762        assert!(names.contains(&"reddb_query"));
763        assert!(names.contains(&"reddb_collections"));
764        assert!(names.contains(&"reddb_insert_row"));
765        assert!(names.contains(&"reddb_insert_node"));
766        assert!(names.contains(&"reddb_insert_edge"));
767        assert!(names.contains(&"reddb_insert_vector"));
768        assert!(names.contains(&"reddb_insert_document"));
769        assert!(names.contains(&"reddb_kv_get"));
770        assert!(names.contains(&"reddb_kv_set"));
771        assert!(names.contains(&"reddb_config_get"));
772        assert!(names.contains(&"reddb_config_put"));
773        assert!(names.contains(&"reddb_config_resolve"));
774        assert!(names.contains(&"reddb_vault_get"));
775        assert!(names.contains(&"reddb_vault_put"));
776        assert!(names.contains(&"reddb_vault_unseal"));
777        assert!(names.contains(&"reddb_delete"));
778        assert!(names.contains(&"reddb_search_vector"));
779        assert!(names.contains(&"reddb_search_text"));
780        assert!(names.contains(&"reddb_health"));
781        assert!(names.contains(&"reddb_graph_traverse"));
782        assert!(names.contains(&"reddb_graph_shortest_path"));
783        // New tools
784        assert!(names.contains(&"reddb_update"));
785        assert!(names.contains(&"reddb_scan"));
786        assert!(names.contains(&"reddb_graph_centrality"));
787        assert!(names.contains(&"reddb_graph_community"));
788        assert!(names.contains(&"reddb_graph_components"));
789        assert!(names.contains(&"reddb_graph_cycles"));
790        assert!(names.contains(&"reddb_graph_clustering"));
791        assert!(names.contains(&"reddb_create_collection"));
792        assert!(names.contains(&"reddb_drop_collection"));
793        // RQL knowledge tools (ADR 0061).
794        assert!(names.contains(&"reddb_rql_validate"));
795        assert!(names.contains(&"reddb_rql_explain"));
796        // Type knowledge tool (ADR 0061).
797        assert!(names.contains(&"reddb_type_of"));
798        // Connection knowledge tool (ADR 0061).
799        assert!(names.contains(&"reddb_explain_connection"));
800    }
801
802    #[test]
803    fn test_rql_validate_tool_schema() {
804        let tools = all_tools();
805        let tool = tools
806            .iter()
807            .find(|t| t.name == "reddb_rql_validate")
808            .expect("reddb_rql_validate registered");
809        let props = tool
810            .input_schema
811            .get("properties")
812            .and_then(|v| v.as_object())
813            .unwrap();
814        assert!(props.contains_key("rql"));
815        let required = tool
816            .input_schema
817            .get("required")
818            .and_then(|v| v.as_array())
819            .unwrap();
820        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
821        assert!(required_strs.contains(&"rql"));
822    }
823
824    #[test]
825    fn test_rql_explain_tool_schema() {
826        let tools = all_tools();
827        let tool = tools
828            .iter()
829            .find(|t| t.name == "reddb_rql_explain")
830            .expect("reddb_rql_explain registered");
831        let props = tool
832            .input_schema
833            .get("properties")
834            .and_then(|v| v.as_object())
835            .unwrap();
836        assert!(props.contains_key("rql"));
837        let required = tool
838            .input_schema
839            .get("required")
840            .and_then(|v| v.as_array())
841            .unwrap();
842        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
843        assert!(required_strs.contains(&"rql"));
844    }
845
846    #[test]
847    fn test_tool_schemas_have_type() {
848        for tool in all_tools() {
849            assert_eq!(
850                tool.input_schema.get("type").and_then(|v| v.as_str()),
851                Some("object"),
852                "tool '{}' schema must have type=object",
853                tool.name,
854            );
855        }
856    }
857
858    #[test]
859    fn test_query_tool_schema_exposes_optional_params_array() {
860        let tools = all_tools();
861        let tool = tools.iter().find(|t| t.name == "reddb_query").unwrap();
862        let props = tool
863            .input_schema
864            .get("properties")
865            .and_then(|v| v.as_object())
866            .unwrap();
867        let params = props.get("params").and_then(|v| v.as_object()).unwrap();
868        assert_eq!(params.get("type").and_then(|v| v.as_str()), Some("array"));
869
870        let required = tool
871            .input_schema
872            .get("required")
873            .and_then(|v| v.as_array())
874            .unwrap();
875        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
876        assert!(required_strs.contains(&"sql"));
877        assert!(!required_strs.contains(&"params"));
878    }
879
880    #[test]
881    fn test_update_tool_schema() {
882        let tools = all_tools();
883        let tool = tools.iter().find(|t| t.name == "reddb_update").unwrap();
884        assert_eq!(tool.name, "reddb_update");
885        let props = tool
886            .input_schema
887            .get("properties")
888            .and_then(|v| v.as_object())
889            .unwrap();
890        assert!(props.contains_key("collection"));
891        assert!(props.contains_key("set"));
892        assert!(props.contains_key("where_filter"));
893        let required = tool
894            .input_schema
895            .get("required")
896            .and_then(|v| v.as_array())
897            .unwrap();
898        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
899        assert!(required_strs.contains(&"collection"));
900        assert!(required_strs.contains(&"set"));
901        assert!(!required_strs.contains(&"where_filter"));
902    }
903
904    #[test]
905    fn test_scan_tool_schema() {
906        let tools = all_tools();
907        let tool = tools.iter().find(|t| t.name == "reddb_scan").unwrap();
908        let props = tool
909            .input_schema
910            .get("properties")
911            .and_then(|v| v.as_object())
912            .unwrap();
913        assert!(props.contains_key("collection"));
914        assert!(props.contains_key("limit"));
915        assert!(props.contains_key("offset"));
916        let required = tool
917            .input_schema
918            .get("required")
919            .and_then(|v| v.as_array())
920            .unwrap();
921        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
922        assert!(required_strs.contains(&"collection"));
923        // limit and offset are optional
924        assert!(!required_strs.contains(&"limit"));
925        assert!(!required_strs.contains(&"offset"));
926    }
927
928    #[test]
929    fn test_graph_centrality_tool_schema() {
930        let tools = all_tools();
931        let tool = tools
932            .iter()
933            .find(|t| t.name == "reddb_graph_centrality")
934            .unwrap();
935        let props = tool
936            .input_schema
937            .get("properties")
938            .and_then(|v| v.as_object())
939            .unwrap();
940        assert!(props.contains_key("algorithm"));
941        let required = tool
942            .input_schema
943            .get("required")
944            .and_then(|v| v.as_array())
945            .unwrap();
946        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
947        assert!(required_strs.contains(&"algorithm"));
948    }
949
950    #[test]
951    fn test_graph_community_tool_schema() {
952        let tools = all_tools();
953        let tool = tools
954            .iter()
955            .find(|t| t.name == "reddb_graph_community")
956            .unwrap();
957        let props = tool
958            .input_schema
959            .get("properties")
960            .and_then(|v| v.as_object())
961            .unwrap();
962        assert!(props.contains_key("algorithm"));
963        assert!(props.contains_key("max_iterations"));
964        let required = tool
965            .input_schema
966            .get("required")
967            .and_then(|v| v.as_array())
968            .unwrap();
969        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
970        assert!(required_strs.contains(&"algorithm"));
971        assert!(!required_strs.contains(&"max_iterations"));
972    }
973
974    #[test]
975    fn test_graph_components_tool_schema() {
976        let tools = all_tools();
977        let tool = tools
978            .iter()
979            .find(|t| t.name == "reddb_graph_components")
980            .unwrap();
981        let props = tool
982            .input_schema
983            .get("properties")
984            .and_then(|v| v.as_object())
985            .unwrap();
986        assert!(props.contains_key("mode"));
987        let required = tool
988            .input_schema
989            .get("required")
990            .and_then(|v| v.as_array())
991            .unwrap();
992        // mode is optional (has default)
993        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
994        assert!(required_strs.is_empty());
995    }
996
997    #[test]
998    fn test_graph_cycles_tool_schema() {
999        let tools = all_tools();
1000        let tool = tools
1001            .iter()
1002            .find(|t| t.name == "reddb_graph_cycles")
1003            .unwrap();
1004        let props = tool
1005            .input_schema
1006            .get("properties")
1007            .and_then(|v| v.as_object())
1008            .unwrap();
1009        assert!(props.contains_key("max_length"));
1010        assert!(props.contains_key("max_cycles"));
1011        let required = tool
1012            .input_schema
1013            .get("required")
1014            .and_then(|v| v.as_array())
1015            .unwrap();
1016        // All optional
1017        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1018        assert!(required_strs.is_empty());
1019    }
1020
1021    #[test]
1022    fn test_graph_clustering_tool_schema() {
1023        let tools = all_tools();
1024        let tool = tools
1025            .iter()
1026            .find(|t| t.name == "reddb_graph_clustering")
1027            .unwrap();
1028        let props = tool
1029            .input_schema
1030            .get("properties")
1031            .and_then(|v| v.as_object())
1032            .unwrap();
1033        // No required properties - takes no arguments
1034        assert!(props.is_empty());
1035    }
1036
1037    #[test]
1038    fn test_create_collection_tool_schema() {
1039        let tools = all_tools();
1040        let tool = tools
1041            .iter()
1042            .find(|t| t.name == "reddb_create_collection")
1043            .unwrap();
1044        let props = tool
1045            .input_schema
1046            .get("properties")
1047            .and_then(|v| v.as_object())
1048            .unwrap();
1049        assert!(props.contains_key("name"));
1050        let name_type = props
1051            .get("name")
1052            .and_then(|v| v.get("type"))
1053            .and_then(|v| v.as_str());
1054        assert_eq!(name_type, Some("string"));
1055        let required = tool
1056            .input_schema
1057            .get("required")
1058            .and_then(|v| v.as_array())
1059            .unwrap();
1060        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1061        assert!(required_strs.contains(&"name"));
1062    }
1063
1064    #[test]
1065    fn test_drop_collection_tool_schema() {
1066        let tools = all_tools();
1067        let tool = tools
1068            .iter()
1069            .find(|t| t.name == "reddb_drop_collection")
1070            .unwrap();
1071        let props = tool
1072            .input_schema
1073            .get("properties")
1074            .and_then(|v| v.as_object())
1075            .unwrap();
1076        assert!(props.contains_key("name"));
1077        let required = tool
1078            .input_schema
1079            .get("required")
1080            .and_then(|v| v.as_array())
1081            .unwrap();
1082        let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1083        assert!(required_strs.contains(&"name"));
1084    }
1085
1086    #[test]
1087    fn test_type_of_tool_schema() {
1088        let tools = all_tools();
1089        let tool = tools.iter().find(|t| t.name == "reddb_type_of").unwrap();
1090        let props = tool
1091            .input_schema
1092            .get("properties")
1093            .and_then(|v| v.as_object())
1094            .unwrap();
1095        // Accepts either a type name or a literal value; neither is required so
1096        // the handler can validate "exactly one of" and surface a clear error.
1097        assert!(props.contains_key("type"));
1098        assert!(props.contains_key("value"));
1099        let required = tool
1100            .input_schema
1101            .get("required")
1102            .and_then(|v| v.as_array())
1103            .unwrap();
1104        assert!(required.is_empty());
1105    }
1106}