1use crate::json::{Map, Value as JsonValue};
7
8pub struct ToolDef {
10 pub name: &'static str,
11 pub description: &'static str,
12 pub input_schema: JsonValue,
13}
14
15pub 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
29pub 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
59fn 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
93fn 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
116fn 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
127fn 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
138fn 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
149fn 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
169fn 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
180fn 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
195fn 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
210pub 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 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 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 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 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 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 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 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 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 assert!(names.contains(&"reddb_rql_validate"));
795 assert!(names.contains(&"reddb_rql_explain"));
796 assert!(names.contains(&"reddb_type_of"));
798 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 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 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 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 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 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}