1use crate::application::{OperationContextFactory, OperationContextInput};
7use crate::json::{Map, Value as JsonValue};
8use crate::server::route_catalog::{
9 CommandAuthorizer, CommandPolicyEngine, CommandSpec, RouteAuth,
10};
11
12pub const ASK_TOOL_COMMAND_ID: &str = "ai.ask";
16
17pub struct ToolDef {
19 pub name: &'static str,
20 pub description: &'static str,
21 pub command_id: &'static str,
22 pub input_schema: JsonValue,
23}
24
25impl ToolDef {
26 pub fn descriptor_description(&self) -> String {
27 descriptor_description(self.name, self.description, self.command_id)
28 }
29}
30
31pub fn ask_descriptor() -> JsonValue {
32 let mut descriptor = crate::runtime::ai::mcp_ask_tool::descriptor();
33 let description = descriptor
34 .get("description")
35 .and_then(JsonValue::as_str)
36 .expect("reddb.ask descriptor has a description");
37 let decorated = descriptor_description(
38 crate::runtime::ai::mcp_ask_tool::TOOL_NAME,
39 description,
40 ASK_TOOL_COMMAND_ID,
41 );
42 let JsonValue::Object(object) = &mut descriptor else {
43 panic!("reddb.ask descriptor is an object");
44 };
45 object.insert("description".to_string(), JsonValue::String(decorated));
46 descriptor
47}
48
49fn descriptor_description(tool_name: &str, description: &str, command_id: &str) -> String {
50 let command = crate::server::command_catalog()
51 .command(command_id)
52 .unwrap_or_else(|| panic!("MCP tool {tool_name} maps to unknown command {command_id}"));
53 format!(
54 "{description}\n\nAuthorization: Embedded/local mode runs as implicit admin because the process owns the database. Remote mode requires credentials before dispatching canonical command `{}` ({}); the remote transport verifies those credentials and enforces IAM policy.",
55 command.id,
56 command_auth_label(command.auth),
57 )
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum McpIdentityPosture {
62 EmbeddedImplicitAdmin,
63 RemoteCredentialsPresented,
64 RemoteCredentialsMissing,
65}
66
67struct McpCommandPolicyEngine {
68 posture: McpIdentityPosture,
69}
70
71impl CommandPolicyEngine for McpCommandPolicyEngine {
72 fn allows(
73 &self,
74 _operation_context: &crate::application::OperationContext,
75 _command: &CommandSpec,
76 ) -> bool {
77 !matches!(self.posture, McpIdentityPosture::RemoteCredentialsMissing)
83 }
84}
85
86pub fn authorize_tool(
87 tool_name: &str,
88 posture: McpIdentityPosture,
89) -> Result<&'static str, String> {
90 let command_id = if tool_name == crate::runtime::ai::mcp_ask_tool::TOOL_NAME {
91 ASK_TOOL_COMMAND_ID
92 } else {
93 all_tools()
94 .into_iter()
95 .find(|tool| tool.name == tool_name)
96 .map(|tool| tool.command_id)
97 .ok_or_else(|| format!("unknown tool: {tool_name}"))?
98 };
99 let principal = match posture {
100 McpIdentityPosture::EmbeddedImplicitAdmin => Some("mcp:embedded-admin".to_string()),
101 McpIdentityPosture::RemoteCredentialsPresented => {
102 Some("mcp:remote-credentials-presented".to_string())
103 }
104 McpIdentityPosture::RemoteCredentialsMissing => None,
105 };
106 let operation_context = OperationContextFactory::build(OperationContextInput {
107 principal,
108 ..OperationContextInput::default()
109 });
110 let engine = McpCommandPolicyEngine { posture };
111 CommandAuthorizer::new(crate::server::command_catalog(), &engine)
112 .authorize(&operation_context, command_id)
113 .map_err(|error| match posture {
114 McpIdentityPosture::RemoteCredentialsMissing => {
115 format!("remote MCP requires credentials: {error}")
116 }
117 _ => error.to_string(),
118 })?;
119 Ok(command_id)
120}
121
122fn command_auth_label(auth: RouteAuth) -> &'static str {
123 match auth {
124 RouteAuth::Undeclared => "undeclared",
125 RouteAuth::Public => "public",
126 RouteAuth::OptionalUser => "optional user",
127 RouteAuth::UserRequired => "user required",
128 RouteAuth::AdminToken => "admin token",
129 RouteAuth::OpsCapability(_) => "operations capability",
130 RouteAuth::StreamLease => "stream lease",
131 }
132}
133
134pub struct ResourceDef {
141 pub uri: &'static str,
142 pub title: &'static str,
143 pub description: &'static str,
144 pub mime_type: &'static str,
145 pub body: fn() -> String,
146}
147
148pub fn knowledge_resources() -> Vec<ResourceDef> {
153 vec![
154 ResourceDef {
155 uri: reddb_rql::knowledge::RESOURCE_URI,
156 title: reddb_rql::knowledge::RESOURCE_TITLE,
157 description: reddb_rql::knowledge::RESOURCE_DESCRIPTION,
158 mime_type: "text/markdown",
159 body: reddb_rql::knowledge::rql_reference_markdown,
160 },
161 ResourceDef {
162 uri: reddb_types::knowledge::RESOURCE_URI,
163 title: reddb_types::knowledge::RESOURCE_TITLE,
164 description: reddb_types::knowledge::RESOURCE_DESCRIPTION,
165 mime_type: "text/markdown",
166 body: reddb_types::knowledge::type_reference_markdown,
167 },
168 ResourceDef {
169 uri: reddb_wire::knowledge::RESOURCE_URI,
170 title: reddb_wire::knowledge::RESOURCE_TITLE,
171 description: reddb_wire::knowledge::RESOURCE_DESCRIPTION,
172 mime_type: "text/markdown",
173 body: reddb_wire::knowledge::connection_reference_markdown,
174 },
175 ]
176}
177
178fn schema(properties: Vec<(&str, &str, &str)>, required: Vec<&str>) -> JsonValue {
180 let mut props = Map::new();
181 for (name, field_type, description) in properties {
182 let mut field = Map::new();
183 field.insert(
184 "type".to_string(),
185 JsonValue::String(field_type.to_string()),
186 );
187 if !description.is_empty() {
188 field.insert(
189 "description".to_string(),
190 JsonValue::String(description.to_string()),
191 );
192 }
193 props.insert(name.to_string(), JsonValue::Object(field));
194 }
195
196 let mut obj = Map::new();
197 obj.insert("type".to_string(), JsonValue::String("object".to_string()));
198 obj.insert("properties".to_string(), JsonValue::Object(props));
199 obj.insert(
200 "required".to_string(),
201 JsonValue::Array(
202 required
203 .into_iter()
204 .map(|s| JsonValue::String(s.to_string()))
205 .collect(),
206 ),
207 );
208 obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
209 JsonValue::Object(obj)
210}
211
212fn schema_with_nested(properties: Vec<(&str, JsonValue)>, required: Vec<&str>) -> JsonValue {
214 let mut props = Map::new();
215 for (name, descriptor) in properties {
216 props.insert(name.to_string(), descriptor);
217 }
218
219 let mut obj = Map::new();
220 obj.insert("type".to_string(), JsonValue::String("object".to_string()));
221 obj.insert("properties".to_string(), JsonValue::Object(props));
222 obj.insert(
223 "required".to_string(),
224 JsonValue::Array(
225 required
226 .into_iter()
227 .map(|s| JsonValue::String(s.to_string()))
228 .collect(),
229 ),
230 );
231 obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
232 JsonValue::Object(obj)
233}
234
235fn string_field(description: &str) -> JsonValue {
237 let mut f = Map::new();
238 f.insert("type".to_string(), JsonValue::String("string".to_string()));
239 f.insert(
240 "description".to_string(),
241 JsonValue::String(description.to_string()),
242 );
243 JsonValue::Object(f)
244}
245
246fn number_field(description: &str) -> JsonValue {
248 let mut f = Map::new();
249 f.insert("type".to_string(), JsonValue::String("number".to_string()));
250 f.insert(
251 "description".to_string(),
252 JsonValue::String(description.to_string()),
253 );
254 JsonValue::Object(f)
255}
256
257fn integer_field(description: &str) -> JsonValue {
259 let mut f = Map::new();
260 f.insert("type".to_string(), JsonValue::String("integer".to_string()));
261 f.insert(
262 "description".to_string(),
263 JsonValue::String(description.to_string()),
264 );
265 JsonValue::Object(f)
266}
267
268fn boolean_field(description: &str) -> JsonValue {
270 let mut f = Map::new();
271 f.insert("type".to_string(), JsonValue::String("boolean".to_string()));
272 f.insert(
273 "description".to_string(),
274 JsonValue::String(description.to_string()),
275 );
276 JsonValue::Object(f)
277}
278
279fn type_field(field_type: &str) -> JsonValue {
280 let mut f = Map::new();
281 f.insert(
282 "type".to_string(),
283 JsonValue::String(field_type.to_string()),
284 );
285 JsonValue::Object(f)
286}
287
288fn object_field(description: &str) -> JsonValue {
290 let mut f = Map::new();
291 f.insert("type".to_string(), JsonValue::String("object".to_string()));
292 f.insert(
293 "description".to_string(),
294 JsonValue::String(description.to_string()),
295 );
296 JsonValue::Object(f)
297}
298
299fn number_array_field(description: &str) -> JsonValue {
301 let mut items = Map::new();
302 items.insert("type".to_string(), JsonValue::String("number".to_string()));
303
304 let mut f = Map::new();
305 f.insert("type".to_string(), JsonValue::String("array".to_string()));
306 f.insert("items".to_string(), JsonValue::Object(items));
307 f.insert(
308 "description".to_string(),
309 JsonValue::String(description.to_string()),
310 );
311 JsonValue::Object(f)
312}
313
314fn string_array_field(description: &str) -> JsonValue {
316 let mut items = Map::new();
317 items.insert("type".to_string(), JsonValue::String("string".to_string()));
318
319 let mut f = Map::new();
320 f.insert("type".to_string(), JsonValue::String("array".to_string()));
321 f.insert("items".to_string(), JsonValue::Object(items));
322 f.insert(
323 "description".to_string(),
324 JsonValue::String(description.to_string()),
325 );
326 JsonValue::Object(f)
327}
328
329pub fn all_tools() -> Vec<ToolDef> {
331 vec![
332 ToolDef {
333 name: "reddb_query",
334 command_id: "query.execute",
335 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.",
336 input_schema: schema_with_nested(
337 vec![
338 ("sql", string_field("SQL or universal query to execute. Use `$1`, `$2`, ... placeholders for any value that came from the user.")),
339 ("params", {
340 let mut items = Map::new();
341 items.insert("description".to_string(), JsonValue::String("Bind value for the matching $N placeholder. Accepts null, boolean, number, string, array, or object.".to_string()));
342 items.insert(
343 "anyOf".to_string(),
344 JsonValue::Array(vec![
345 type_field("null"),
346 type_field("boolean"),
347 type_field("number"),
348 type_field("string"),
349 type_field("array"),
350 type_field("object"),
351 ]),
352 );
353 let mut f = Map::new();
354 f.insert("type".to_string(), JsonValue::String("array".to_string()));
355 f.insert("items".to_string(), JsonValue::Object(items));
356 f.insert(
357 "description".to_string(),
358 JsonValue::String(
359 "Positional bind values for `$1`, `$2`, ... in `sql`. Index 0 binds `$1`."
360 .to_string(),
361 ),
362 );
363 JsonValue::Object(f)
364 }),
365 ],
366 vec!["sql"],
367 ),
368 },
369 ToolDef {
370 name: "reddb_collections",
371 command_id: "collections.list",
372 description: "List the names of collections visible to the connected identity.",
373 input_schema: schema(vec![], vec![]),
374 },
375 ToolDef {
376 name: "reddb_catalog",
377 command_id: "query.execute",
378 description: "List visible collections with model, schema, size, index, and attention metadata. Remote calls execute under the connected user's tenant and policies.",
379 input_schema: schema(vec![], vec![]),
380 },
381 ToolDef {
382 name: "reddb_describe_collection",
383 command_id: "query.execute",
384 description: "Describe the fields visible on one collection using the canonical red.columns catalog. The database applies the connected user's tenant and policies before returning metadata.",
385 input_schema: schema(
386 vec![(
387 "collection",
388 "string",
389 "Exact collection name to describe",
390 )],
391 vec!["collection"],
392 ),
393 },
394 ToolDef {
395 name: "reddb_find_field",
396 command_id: "query.execute",
397 description: "Find every visible collection with an exact field name using the canonical red.columns catalog. Useful before a global SELECT when the user names an attribute such as passport_number. Tenant and policy filtering is enforced by the database.",
398 input_schema: schema(
399 vec![(
400 "field",
401 "string",
402 "Exact field or column name to find across visible collections",
403 )],
404 vec!["field"],
405 ),
406 },
407 ToolDef {
408 name: "reddb_insert_row",
409 command_id: "collections.rows.create",
410 description: "Insert a table row into a collection.",
411 input_schema: schema_with_nested(
412 vec![
413 ("collection", string_field("Target collection name")),
414 ("data", object_field("Object with field name/value pairs to insert")),
415 ("metadata", object_field("Optional metadata key/value pairs")),
416 ],
417 vec!["collection", "data"],
418 ),
419 },
420 ToolDef {
421 name: "reddb_insert_node",
422 command_id: "collections.nodes.create",
423 description: "Insert a graph node into a collection.",
424 input_schema: schema_with_nested(
425 vec![
426 ("collection", string_field("Target collection name")),
427 ("label", string_field("Node label (identifier)")),
428 ("node_type", string_field("Optional node type classification")),
429 ("properties", object_field("Optional node properties as key/value pairs")),
430 ("metadata", object_field("Optional metadata key/value pairs")),
431 ],
432 vec!["collection", "label"],
433 ),
434 },
435 ToolDef {
436 name: "reddb_insert_edge",
437 command_id: "collections.edges.create",
438 description: "Insert a graph edge between two nodes.",
439 input_schema: schema_with_nested(
440 vec![
441 ("collection", string_field("Target collection name")),
442 ("label", string_field("Edge label (relationship type)")),
443 ("from", integer_field("Source node entity ID")),
444 ("to", integer_field("Target node entity ID")),
445 ("weight", number_field("Optional edge weight (default 1.0)")),
446 ("properties", object_field("Optional edge properties")),
447 ("metadata", object_field("Optional metadata key/value pairs")),
448 ],
449 vec!["collection", "label", "from", "to"],
450 ),
451 },
452 ToolDef {
453 name: "reddb_insert_vector",
454 command_id: "collections.vectors.create",
455 description: "Insert a vector embedding into a collection.",
456 input_schema: schema_with_nested(
457 vec![
458 ("collection", string_field("Target collection name")),
459 ("dense", number_array_field("Dense vector (array of floats)")),
460 ("content", string_field("Optional text content associated with the vector")),
461 ("metadata", object_field("Optional metadata key/value pairs")),
462 ],
463 vec!["collection", "dense"],
464 ),
465 },
466 ToolDef {
467 name: "reddb_insert_document",
468 command_id: "collections.documents.create",
469 description: "Insert a JSON document into a collection.",
470 input_schema: schema_with_nested(
471 vec![
472 ("collection", string_field("Target collection name")),
473 ("body", object_field("JSON document body")),
474 ("metadata", object_field("Optional metadata key/value pairs")),
475 ],
476 vec!["collection", "body"],
477 ),
478 },
479 ToolDef {
480 name: "reddb_kv_get",
481 command_id: "keyed.v1.kv",
482 description: "Get a value by key from a key-value collection.",
483 input_schema: schema(
484 vec![
485 ("collection", "string", "Collection name"),
486 ("key", "string", "Key to retrieve"),
487 ],
488 vec!["collection", "key"],
489 ),
490 },
491 ToolDef {
492 name: "reddb_kv_set",
493 command_id: "keyed.v1.kv",
494 description: "Set a key-value pair in a collection.",
495 input_schema: schema_with_nested(
496 vec![
497 ("collection", string_field("Collection name")),
498 ("key", string_field("Key to set")),
499 ("value", {
500 let mut f = Map::new();
501 f.insert("description".to_string(), JsonValue::String("Value to store (string, number, boolean, or null)".to_string()));
502 JsonValue::Object(f)
503 }),
504 ("tags", string_array_field("Optional KV invalidation tags")),
505 ("metadata", object_field("Optional metadata key/value pairs")),
506 ],
507 vec!["collection", "key", "value"],
508 ),
509 },
510 ToolDef {
511 name: "reddb_kv_invalidate_tags",
512 command_id: "keyed.v1.kv",
513 description: "Delete every KV entry in a collection tagged with any listed tag.",
514 input_schema: schema_with_nested(
515 vec![
516 ("collection", string_field("Collection name")),
517 ("tags", string_array_field("Tags to invalidate")),
518 ],
519 vec!["collection", "tags"],
520 ),
521 },
522 ToolDef {
523 name: "reddb_config_get",
524 command_id: "keyed.v1.config",
525 description: "Get a Config value without resolving SecretRef targets.",
526 input_schema: schema(
527 vec![
528 ("collection", "string", "Config collection name"),
529 ("key", "string", "Config key to retrieve"),
530 ],
531 vec!["collection", "key"],
532 ),
533 },
534 ToolDef {
535 name: "reddb_config_put",
536 command_id: "keyed.v1.config",
537 description: "Set a Config value. TTL and counter operations are not supported for Config.",
538 input_schema: schema_with_nested(
539 vec![
540 ("collection", string_field("Config collection name")),
541 ("key", string_field("Config key to set")),
542 ("value", {
543 let mut f = Map::new();
544 f.insert("description".to_string(), JsonValue::String("Value to store, or an object when paired with secret_ref".to_string()));
545 JsonValue::Object(f)
546 }),
547 ("secret_ref", object_field("Optional { collection, key } Vault SecretRef")),
548 ("tags", string_array_field("Optional Config tags")),
549 ],
550 vec!["collection", "key"],
551 ),
552 },
553 ToolDef {
554 name: "reddb_config_resolve",
555 command_id: "keyed.v1.config",
556 description: "Explicitly resolve a Config SecretRef. Requires the corresponding Vault unseal permission.",
557 input_schema: schema(
558 vec![
559 ("collection", "string", "Config collection name"),
560 ("key", "string", "Config key to resolve"),
561 ],
562 vec!["collection", "key"],
563 ),
564 },
565 ToolDef {
566 name: "reddb_vault_get",
567 command_id: "keyed.v1.vault",
568 description: "Get Vault metadata for a secret. Does not return plaintext.",
569 input_schema: schema(
570 vec![
571 ("collection", "string", "Vault collection name"),
572 ("key", "string", "Vault key to retrieve metadata for"),
573 ],
574 vec!["collection", "key"],
575 ),
576 },
577 ToolDef {
578 name: "reddb_vault_put",
579 command_id: "keyed.v1.vault",
580 description: "Store a sealed Vault secret. TTL and counter operations are not supported for Vault.",
581 input_schema: schema_with_nested(
582 vec![
583 ("collection", string_field("Vault collection name")),
584 ("key", string_field("Vault key to set")),
585 ("value", {
586 let mut f = Map::new();
587 f.insert("description".to_string(), JsonValue::String("Secret value to seal".to_string()));
588 JsonValue::Object(f)
589 }),
590 ("tags", string_array_field("Optional Vault tags")),
591 ],
592 vec!["collection", "key", "value"],
593 ),
594 },
595 ToolDef {
596 name: "reddb_vault_reveal",
597 command_id: "keyed.v1.vault",
598 description: "Explicitly reveal a Vault secret to a caller with vault:reveal permission.",
599 input_schema: schema(
600 vec![("collection", "string", "Vault collection name"), ("key", "string", "Vault key to reveal")],
601 vec!["collection", "key"],
602 ),
603 },
604 ToolDef {
605 name: "reddb_vault_unseal",
606 command_id: "keyed.v1.vault",
607 description: "Explicitly unseal a Vault secret and return plaintext to an authorized caller.",
608 input_schema: schema(
609 vec![
610 ("collection", "string", "Vault collection name"),
611 ("key", "string", "Vault key to unseal"),
612 ],
613 vec!["collection", "key"],
614 ),
615 },
616 ToolDef {
617 name: "reddb_delete",
618 command_id: "collections.entities.delete",
619 description: "Delete an entity by ID from a collection.",
620 input_schema: schema(
621 vec![
622 ("collection", "string", "Collection name"),
623 ("id", "integer", "Entity ID to delete"),
624 ],
625 vec!["collection", "id"],
626 ),
627 },
628 ToolDef {
629 name: "reddb_search_vector",
630 command_id: "collections.similar",
631 description: "Search for similar vectors using cosine similarity.",
632 input_schema: schema_with_nested(
633 vec![
634 ("collection", string_field("Collection to search in")),
635 ("vector", number_array_field("Query vector (array of floats)")),
636 ("k", integer_field("Number of results to return (default 10)")),
637 ("min_score", number_field("Minimum similarity score threshold (default 0.0)")),
638 ],
639 vec!["collection", "vector"],
640 ),
641 },
642 ToolDef {
643 name: "reddb_search_text",
644 command_id: "query.text_search",
645 description: "Full-text search across collections.",
646 input_schema: schema_with_nested(
647 vec![
648 ("query", string_field("Search query string")),
649 ("collections", {
650 let mut items = Map::new();
651 items.insert("type".to_string(), JsonValue::String("string".to_string()));
652 let mut f = Map::new();
653 f.insert("type".to_string(), JsonValue::String("array".to_string()));
654 f.insert("items".to_string(), JsonValue::Object(items));
655 f.insert("description".to_string(), JsonValue::String("Optional list of collections to search".to_string()));
656 JsonValue::Object(f)
657 }),
658 ("limit", integer_field("Maximum number of results (default 10)")),
659 ("fuzzy", boolean_field("Enable fuzzy matching (default false)")),
660 ],
661 vec!["query"],
662 ),
663 },
664 ToolDef {
665 name: "reddb_health",
666 command_id: "health.live",
667 description: "Check database health and return runtime statistics.",
668 input_schema: schema(vec![], vec![]),
669 },
670 ToolDef {
671 name: "reddb_graph_traverse",
672 command_id: "graph.traverse",
673 description: "Traverse the graph from a source node using BFS or DFS.",
674 input_schema: schema_with_nested(
675 vec![
676 ("source", string_field("Source node label to start traversal from")),
677 ("direction", string_field("Traversal direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
678 ("max_depth", integer_field("Maximum traversal depth (default 3)")),
679 ("strategy", string_field("Traversal strategy: 'bfs' or 'dfs' (default 'bfs')")),
680 ],
681 vec!["source"],
682 ),
683 },
684 ToolDef {
685 name: "reddb_graph_shortest_path",
686 command_id: "graph.shortest_path",
687 description: "Find the shortest path between two graph nodes.",
688 input_schema: schema_with_nested(
689 vec![
690 ("source", string_field("Source node label")),
691 ("target", string_field("Target node label")),
692 ("direction", string_field("Edge direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
693 (
694 "algorithm",
695 string_field(
696 "Path algorithm: 'bfs', 'dijkstra', 'astar', or 'bellman_ford' (default 'bfs')",
697 ),
698 ),
699 ],
700 vec!["source", "target"],
701 ),
702 },
703 ToolDef {
705 name: "reddb_auth_bootstrap",
706 command_id: "auth.bootstrap",
707 description: "Bootstrap the first admin user. Only works when no users exist yet. Returns the admin user and an API key.",
708 input_schema: schema(
709 vec![
710 ("username", "string", "Admin username"),
711 ("password", "string", "Admin password"),
712 ],
713 vec!["username", "password"],
714 ),
715 },
716 ToolDef {
717 name: "reddb_auth_create_user",
718 command_id: "auth.users.create",
719 description: "Create a new database user with a role (admin, write, or read).",
720 input_schema: schema(
721 vec![
722 ("username", "string", "Username for the new user"),
723 ("password", "string", "Password for the new user"),
724 ("role", "string", "Role: 'admin', 'write', or 'read'"),
725 ],
726 vec!["username", "password", "role"],
727 ),
728 },
729 ToolDef {
730 name: "reddb_auth_login",
731 command_id: "auth.login",
732 description: "Login with username and password. Returns a session token.",
733 input_schema: schema(
734 vec![
735 ("username", "string", "Username"),
736 ("password", "string", "Password"),
737 ],
738 vec!["username", "password"],
739 ),
740 },
741 ToolDef {
742 name: "reddb_auth_create_api_key",
743 command_id: "auth.api_keys.create",
744 description: "Create a persistent API key for a user.",
745 input_schema: schema(
746 vec![
747 ("username", "string", "Username to create the key for"),
748 ("name", "string", "Human-readable label for the key"),
749 ("role", "string", "Role for this key: 'admin', 'write', or 'read'"),
750 ],
751 vec!["username", "name", "role"],
752 ),
753 },
754 ToolDef {
755 name: "reddb_auth_list_users",
756 command_id: "auth.users.list",
757 description: "List all database users and their roles.",
758 input_schema: schema(vec![], vec![]),
759 },
760 ToolDef {
762 name: "reddb_update",
763 command_id: "collections.entities.patch",
764 description: "Update entities in a collection matching a filter.",
765 input_schema: schema_with_nested(
766 vec![
767 ("collection", string_field("Collection name")),
768 ("set", object_field("Key-value pairs to update")),
769 (
770 "where_filter",
771 string_field(
772 "Optional SQL WHERE clause (e.g., \"age > 21\")",
773 ),
774 ),
775 ],
776 vec!["collection", "set"],
777 ),
778 },
779 ToolDef {
780 name: "reddb_scan",
781 command_id: "collections.scan",
782 description: "Scan entities from a collection with pagination.",
783 input_schema: schema_with_nested(
784 vec![
785 ("collection", string_field("Collection to scan")),
786 ("limit", integer_field("Maximum number of results (default 10)")),
787 ("offset", integer_field("Number of records to skip (default 0)")),
788 ],
789 vec!["collection"],
790 ),
791 },
792 ToolDef {
794 name: "reddb_graph_centrality",
795 command_id: "graph.analytics.centrality",
796 description: "Compute centrality scores for graph nodes. Algorithms: degree, closeness, betweenness, eigenvector, pagerank.",
797 input_schema: schema_with_nested(
798 vec![(
799 "algorithm",
800 string_field(
801 "Centrality algorithm: 'degree', 'closeness', 'betweenness', 'eigenvector', 'pagerank'",
802 ),
803 )],
804 vec!["algorithm"],
805 ),
806 },
807 ToolDef {
808 name: "reddb_graph_community",
809 command_id: "graph.analytics.community",
810 description: "Detect communities in the graph. Algorithms: label_propagation, louvain.",
811 input_schema: schema_with_nested(
812 vec![
813 (
814 "algorithm",
815 string_field(
816 "Community detection algorithm: 'label_propagation' or 'louvain'",
817 ),
818 ),
819 (
820 "max_iterations",
821 integer_field("Maximum iterations (default 100)"),
822 ),
823 ],
824 vec!["algorithm"],
825 ),
826 },
827 ToolDef {
828 name: "reddb_graph_components",
829 command_id: "graph.analytics.components",
830 description: "Find connected components in the graph.",
831 input_schema: schema_with_nested(
832 vec![(
833 "mode",
834 string_field(
835 "Component mode: 'weakly_connected' or 'strongly_connected' (default 'weakly_connected')",
836 ),
837 )],
838 vec![],
839 ),
840 },
841 ToolDef {
842 name: "reddb_graph_cycles",
843 command_id: "graph.analytics.cycles",
844 description: "Detect cycles in the graph.",
845 input_schema: schema_with_nested(
846 vec![
847 (
848 "max_length",
849 integer_field("Maximum cycle length (default 10)"),
850 ),
851 (
852 "max_cycles",
853 integer_field("Maximum number of cycles to return (default 100)"),
854 ),
855 ],
856 vec![],
857 ),
858 },
859 ToolDef {
860 name: "reddb_graph_clustering",
861 command_id: "graph.analytics.clustering",
862 description: "Compute clustering coefficient for the graph.",
863 input_schema: schema(vec![], vec![]),
864 },
865 ToolDef {
867 name: "reddb_create_collection",
868 command_id: "collections.create",
869 description: "Create a new collection (table) in the database.",
870 input_schema: schema(
871 vec![("name", "string", "Collection name to create")],
872 vec!["name"],
873 ),
874 },
875 ToolDef {
876 name: "reddb_drop_collection",
877 command_id: "collections.drop",
878 description: "Drop (delete) a collection from the database.",
879 input_schema: schema(
880 vec![("name", "string", "Collection name to drop")],
881 vec!["name"],
882 ),
883 },
884 ToolDef {
888 name: "reddb_rql_validate",
889 command_id: "query.contract",
890 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.",
891 input_schema: schema(
892 vec![("rql", "string", "RQL query string to validate")],
893 vec!["rql"],
894 ),
895 },
896 ToolDef {
897 name: "reddb_rql_explain",
898 command_id: "query.explain",
899 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.",
900 input_schema: schema(
901 vec![("rql", "string", "RQL query string to parse and explain")],
902 vec!["rql"],
903 ),
904 },
905 ToolDef {
908 name: "reddb_type_of",
909 command_id: "catalog.snapshot",
910 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.",
911 input_schema: schema_with_nested(
912 vec![
913 ("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.")),
914 ("value", {
915 let mut f = Map::new();
916 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()));
917 f.insert(
918 "anyOf".to_string(),
919 JsonValue::Array(vec![
920 type_field("null"),
921 type_field("boolean"),
922 type_field("number"),
923 type_field("string"),
924 type_field("array"),
925 type_field("object"),
926 ]),
927 );
928 JsonValue::Object(f)
929 }),
930 ],
931 vec![],
932 ),
933 },
934 ToolDef {
938 name: "reddb_explain_connection",
939 command_id: "ops.capabilities",
940 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.",
941 input_schema: schema(
942 vec![(
943 "uri",
944 "string",
945 "Connection URI to explain, e.g. memory://, file:///data.rdb, reds://host, grpc://host, or https://host",
946 )],
947 vec!["uri"],
948 ),
949 },
950 ]
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 #[test]
958 fn embedded_identity_authorizes_the_full_tool_catalog() {
959 for tool in all_tools() {
960 authorize_tool(&tool.name, McpIdentityPosture::EmbeddedImplicitAdmin)
961 .unwrap_or_else(|err| panic!("embedded tool {} was denied: {err}", tool.name));
962 }
963 authorize_tool(
964 crate::runtime::ai::mcp_ask_tool::TOOL_NAME,
965 McpIdentityPosture::EmbeddedImplicitAdmin,
966 )
967 .expect("embedded reddb.ask is authorized");
968 }
969
970 #[test]
971 fn tool_descriptors_document_catalog_authorization_posture() {
972 for tool in all_tools() {
973 let description = tool.descriptor_description();
974 assert!(
975 description.contains("Embedded/local mode runs as implicit admin"),
976 "embedded posture missing from {}: {}",
977 tool.name,
978 description
979 );
980 assert!(
981 description.contains("Remote mode requires credentials"),
982 "remote posture missing from {}: {}",
983 tool.name,
984 description
985 );
986 assert!(
987 description.contains(tool.command_id),
988 "canonical command id {} missing from {} descriptor",
989 tool.command_id,
990 tool.name
991 );
992 }
993
994 let ask = ask_descriptor();
995 let ask_description = ask
996 .get("description")
997 .and_then(JsonValue::as_str)
998 .expect("reddb.ask description");
999 assert!(ask_description.contains("Embedded/local mode runs as implicit admin"));
1000 assert!(ask_description.contains("Remote mode requires credentials"));
1001 assert!(ask_description.contains("ai.ask"));
1002 }
1003
1004 #[test]
1005 fn test_all_tools_defined() {
1006 let tools = all_tools();
1007 assert!(tools.len() >= 24);
1008 let names: Vec<&str> = tools.iter().map(|t| t.name).collect();
1009 assert!(names.contains(&"reddb_query"));
1010 assert!(names.contains(&"reddb_collections"));
1011 assert!(names.contains(&"reddb_insert_row"));
1012 assert!(names.contains(&"reddb_insert_node"));
1013 assert!(names.contains(&"reddb_insert_edge"));
1014 assert!(names.contains(&"reddb_insert_vector"));
1015 assert!(names.contains(&"reddb_insert_document"));
1016 assert!(names.contains(&"reddb_kv_get"));
1017 assert!(names.contains(&"reddb_kv_set"));
1018 assert!(names.contains(&"reddb_config_get"));
1019 assert!(names.contains(&"reddb_config_put"));
1020 assert!(names.contains(&"reddb_config_resolve"));
1021 assert!(names.contains(&"reddb_vault_get"));
1022 assert!(names.contains(&"reddb_vault_put"));
1023 assert!(names.contains(&"reddb_vault_reveal"));
1024 assert!(names.contains(&"reddb_vault_unseal"));
1025 assert!(names.contains(&"reddb_delete"));
1026 assert!(names.contains(&"reddb_search_vector"));
1027 assert!(names.contains(&"reddb_search_text"));
1028 assert!(names.contains(&"reddb_health"));
1029 assert!(names.contains(&"reddb_graph_traverse"));
1030 assert!(names.contains(&"reddb_graph_shortest_path"));
1031 assert!(names.contains(&"reddb_update"));
1033 assert!(names.contains(&"reddb_scan"));
1034 assert!(names.contains(&"reddb_graph_centrality"));
1035 assert!(names.contains(&"reddb_graph_community"));
1036 assert!(names.contains(&"reddb_graph_components"));
1037 assert!(names.contains(&"reddb_graph_cycles"));
1038 assert!(names.contains(&"reddb_graph_clustering"));
1039 assert!(names.contains(&"reddb_create_collection"));
1040 assert!(names.contains(&"reddb_drop_collection"));
1041 assert!(names.contains(&"reddb_rql_validate"));
1043 assert!(names.contains(&"reddb_rql_explain"));
1044 assert!(names.contains(&"reddb_type_of"));
1046 assert!(names.contains(&"reddb_explain_connection"));
1048 }
1049
1050 #[test]
1051 fn test_rql_validate_tool_schema() {
1052 let tools = all_tools();
1053 let tool = tools
1054 .iter()
1055 .find(|t| t.name == "reddb_rql_validate")
1056 .expect("reddb_rql_validate registered");
1057 let props = tool
1058 .input_schema
1059 .get("properties")
1060 .and_then(|v| v.as_object())
1061 .unwrap();
1062 assert!(props.contains_key("rql"));
1063 let required = tool
1064 .input_schema
1065 .get("required")
1066 .and_then(|v| v.as_array())
1067 .unwrap();
1068 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1069 assert!(required_strs.contains(&"rql"));
1070 }
1071
1072 #[test]
1073 fn test_rql_explain_tool_schema() {
1074 let tools = all_tools();
1075 let tool = tools
1076 .iter()
1077 .find(|t| t.name == "reddb_rql_explain")
1078 .expect("reddb_rql_explain registered");
1079 let props = tool
1080 .input_schema
1081 .get("properties")
1082 .and_then(|v| v.as_object())
1083 .unwrap();
1084 assert!(props.contains_key("rql"));
1085 let required = tool
1086 .input_schema
1087 .get("required")
1088 .and_then(|v| v.as_array())
1089 .unwrap();
1090 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1091 assert!(required_strs.contains(&"rql"));
1092 }
1093
1094 #[test]
1095 fn test_tool_schemas_have_type() {
1096 for tool in all_tools() {
1097 assert_eq!(
1098 tool.input_schema.get("type").and_then(|v| v.as_str()),
1099 Some("object"),
1100 "tool '{}' schema must have type=object",
1101 tool.name,
1102 );
1103 }
1104 }
1105
1106 #[test]
1107 fn test_query_tool_schema_exposes_optional_params_array() {
1108 let tools = all_tools();
1109 let tool = tools.iter().find(|t| t.name == "reddb_query").unwrap();
1110 let props = tool
1111 .input_schema
1112 .get("properties")
1113 .and_then(|v| v.as_object())
1114 .unwrap();
1115 let params = props.get("params").and_then(|v| v.as_object()).unwrap();
1116 assert_eq!(params.get("type").and_then(|v| v.as_str()), Some("array"));
1117
1118 let required = tool
1119 .input_schema
1120 .get("required")
1121 .and_then(|v| v.as_array())
1122 .unwrap();
1123 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1124 assert!(required_strs.contains(&"sql"));
1125 assert!(!required_strs.contains(&"params"));
1126 }
1127
1128 #[test]
1129 fn test_update_tool_schema() {
1130 let tools = all_tools();
1131 let tool = tools.iter().find(|t| t.name == "reddb_update").unwrap();
1132 assert_eq!(tool.name, "reddb_update");
1133 let props = tool
1134 .input_schema
1135 .get("properties")
1136 .and_then(|v| v.as_object())
1137 .unwrap();
1138 assert!(props.contains_key("collection"));
1139 assert!(props.contains_key("set"));
1140 assert!(props.contains_key("where_filter"));
1141 let required = tool
1142 .input_schema
1143 .get("required")
1144 .and_then(|v| v.as_array())
1145 .unwrap();
1146 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1147 assert!(required_strs.contains(&"collection"));
1148 assert!(required_strs.contains(&"set"));
1149 assert!(!required_strs.contains(&"where_filter"));
1150 }
1151
1152 #[test]
1153 fn test_scan_tool_schema() {
1154 let tools = all_tools();
1155 let tool = tools.iter().find(|t| t.name == "reddb_scan").unwrap();
1156 let props = tool
1157 .input_schema
1158 .get("properties")
1159 .and_then(|v| v.as_object())
1160 .unwrap();
1161 assert!(props.contains_key("collection"));
1162 assert!(props.contains_key("limit"));
1163 assert!(props.contains_key("offset"));
1164 let required = tool
1165 .input_schema
1166 .get("required")
1167 .and_then(|v| v.as_array())
1168 .unwrap();
1169 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1170 assert!(required_strs.contains(&"collection"));
1171 assert!(!required_strs.contains(&"limit"));
1173 assert!(!required_strs.contains(&"offset"));
1174 }
1175
1176 #[test]
1177 fn test_graph_centrality_tool_schema() {
1178 let tools = all_tools();
1179 let tool = tools
1180 .iter()
1181 .find(|t| t.name == "reddb_graph_centrality")
1182 .unwrap();
1183 let props = tool
1184 .input_schema
1185 .get("properties")
1186 .and_then(|v| v.as_object())
1187 .unwrap();
1188 assert!(props.contains_key("algorithm"));
1189 let required = tool
1190 .input_schema
1191 .get("required")
1192 .and_then(|v| v.as_array())
1193 .unwrap();
1194 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1195 assert!(required_strs.contains(&"algorithm"));
1196 }
1197
1198 #[test]
1199 fn test_graph_community_tool_schema() {
1200 let tools = all_tools();
1201 let tool = tools
1202 .iter()
1203 .find(|t| t.name == "reddb_graph_community")
1204 .unwrap();
1205 let props = tool
1206 .input_schema
1207 .get("properties")
1208 .and_then(|v| v.as_object())
1209 .unwrap();
1210 assert!(props.contains_key("algorithm"));
1211 assert!(props.contains_key("max_iterations"));
1212 let required = tool
1213 .input_schema
1214 .get("required")
1215 .and_then(|v| v.as_array())
1216 .unwrap();
1217 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1218 assert!(required_strs.contains(&"algorithm"));
1219 assert!(!required_strs.contains(&"max_iterations"));
1220 }
1221
1222 #[test]
1223 fn test_graph_components_tool_schema() {
1224 let tools = all_tools();
1225 let tool = tools
1226 .iter()
1227 .find(|t| t.name == "reddb_graph_components")
1228 .unwrap();
1229 let props = tool
1230 .input_schema
1231 .get("properties")
1232 .and_then(|v| v.as_object())
1233 .unwrap();
1234 assert!(props.contains_key("mode"));
1235 let required = tool
1236 .input_schema
1237 .get("required")
1238 .and_then(|v| v.as_array())
1239 .unwrap();
1240 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1242 assert!(required_strs.is_empty());
1243 }
1244
1245 #[test]
1246 fn test_graph_cycles_tool_schema() {
1247 let tools = all_tools();
1248 let tool = tools
1249 .iter()
1250 .find(|t| t.name == "reddb_graph_cycles")
1251 .unwrap();
1252 let props = tool
1253 .input_schema
1254 .get("properties")
1255 .and_then(|v| v.as_object())
1256 .unwrap();
1257 assert!(props.contains_key("max_length"));
1258 assert!(props.contains_key("max_cycles"));
1259 let required = tool
1260 .input_schema
1261 .get("required")
1262 .and_then(|v| v.as_array())
1263 .unwrap();
1264 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1266 assert!(required_strs.is_empty());
1267 }
1268
1269 #[test]
1270 fn test_graph_clustering_tool_schema() {
1271 let tools = all_tools();
1272 let tool = tools
1273 .iter()
1274 .find(|t| t.name == "reddb_graph_clustering")
1275 .unwrap();
1276 let props = tool
1277 .input_schema
1278 .get("properties")
1279 .and_then(|v| v.as_object())
1280 .unwrap();
1281 assert!(props.is_empty());
1283 }
1284
1285 #[test]
1286 fn test_create_collection_tool_schema() {
1287 let tools = all_tools();
1288 let tool = tools
1289 .iter()
1290 .find(|t| t.name == "reddb_create_collection")
1291 .unwrap();
1292 let props = tool
1293 .input_schema
1294 .get("properties")
1295 .and_then(|v| v.as_object())
1296 .unwrap();
1297 assert!(props.contains_key("name"));
1298 let name_type = props
1299 .get("name")
1300 .and_then(|v| v.get("type"))
1301 .and_then(|v| v.as_str());
1302 assert_eq!(name_type, Some("string"));
1303 let required = tool
1304 .input_schema
1305 .get("required")
1306 .and_then(|v| v.as_array())
1307 .unwrap();
1308 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1309 assert!(required_strs.contains(&"name"));
1310 }
1311
1312 #[test]
1313 fn test_drop_collection_tool_schema() {
1314 let tools = all_tools();
1315 let tool = tools
1316 .iter()
1317 .find(|t| t.name == "reddb_drop_collection")
1318 .unwrap();
1319 let props = tool
1320 .input_schema
1321 .get("properties")
1322 .and_then(|v| v.as_object())
1323 .unwrap();
1324 assert!(props.contains_key("name"));
1325 let required = tool
1326 .input_schema
1327 .get("required")
1328 .and_then(|v| v.as_array())
1329 .unwrap();
1330 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1331 assert!(required_strs.contains(&"name"));
1332 }
1333
1334 #[test]
1335 fn test_type_of_tool_schema() {
1336 let tools = all_tools();
1337 let tool = tools.iter().find(|t| t.name == "reddb_type_of").unwrap();
1338 let props = tool
1339 .input_schema
1340 .get("properties")
1341 .and_then(|v| v.as_object())
1342 .unwrap();
1343 assert!(props.contains_key("type"));
1346 assert!(props.contains_key("value"));
1347 let required = tool
1348 .input_schema
1349 .get("required")
1350 .and_then(|v| v.as_array())
1351 .unwrap();
1352 assert!(required.is_empty());
1353 }
1354}