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 let mut tools =
332 crate::runtime::agent_tools::definitions(crate::runtime::agent_tools::ToolSurface::Mcp)
333 .into_iter()
334 .map(|definition| ToolDef {
335 name: definition.name,
336 description: definition.description,
337 command_id: definition.command_id,
338 input_schema: definition.input_schema,
339 })
340 .collect::<Vec<_>>();
341 tools.extend(vec![
342 ToolDef {
343 name: "reddb_collections",
344 command_id: "collections.list",
345 description: "List the names of collections visible to the connected identity.",
346 input_schema: schema(vec![], vec![]),
347 },
348 ToolDef {
349 name: "reddb_insert_row",
350 command_id: "collections.rows.create",
351 description: "Insert a table row into a collection.",
352 input_schema: schema_with_nested(
353 vec![
354 ("collection", string_field("Target collection name")),
355 ("data", object_field("Object with field name/value pairs to insert")),
356 ("metadata", object_field("Optional metadata key/value pairs")),
357 ],
358 vec!["collection", "data"],
359 ),
360 },
361 ToolDef {
362 name: "reddb_insert_node",
363 command_id: "collections.nodes.create",
364 description: "Insert a graph node into a collection.",
365 input_schema: schema_with_nested(
366 vec![
367 ("collection", string_field("Target collection name")),
368 ("label", string_field("Node label (identifier)")),
369 ("node_type", string_field("Optional node type classification")),
370 ("properties", object_field("Optional node properties as key/value pairs")),
371 ("metadata", object_field("Optional metadata key/value pairs")),
372 ],
373 vec!["collection", "label"],
374 ),
375 },
376 ToolDef {
377 name: "reddb_insert_edge",
378 command_id: "collections.edges.create",
379 description: "Insert a graph edge between two nodes.",
380 input_schema: schema_with_nested(
381 vec![
382 ("collection", string_field("Target collection name")),
383 ("label", string_field("Edge label (relationship type)")),
384 ("from", integer_field("Source node entity ID")),
385 ("to", integer_field("Target node entity ID")),
386 ("weight", number_field("Optional edge weight (default 1.0)")),
387 ("properties", object_field("Optional edge properties")),
388 ("metadata", object_field("Optional metadata key/value pairs")),
389 ],
390 vec!["collection", "label", "from", "to"],
391 ),
392 },
393 ToolDef {
394 name: "reddb_insert_vector",
395 command_id: "collections.vectors.create",
396 description: "Insert a vector embedding into a collection.",
397 input_schema: schema_with_nested(
398 vec![
399 ("collection", string_field("Target collection name")),
400 ("dense", number_array_field("Dense vector (array of floats)")),
401 ("content", string_field("Optional text content associated with the vector")),
402 ("metadata", object_field("Optional metadata key/value pairs")),
403 ],
404 vec!["collection", "dense"],
405 ),
406 },
407 ToolDef {
408 name: "reddb_insert_document",
409 command_id: "collections.documents.create",
410 description: "Insert a JSON document into a collection.",
411 input_schema: schema_with_nested(
412 vec![
413 ("collection", string_field("Target collection name")),
414 ("body", object_field("JSON document body")),
415 ("metadata", object_field("Optional metadata key/value pairs")),
416 ],
417 vec!["collection", "body"],
418 ),
419 },
420 ToolDef {
421 name: "reddb_kv_get",
422 command_id: "keyed.v1.kv",
423 description: "Get a value by key from a key-value collection.",
424 input_schema: schema(
425 vec![
426 ("collection", "string", "Collection name"),
427 ("key", "string", "Key to retrieve"),
428 ],
429 vec!["collection", "key"],
430 ),
431 },
432 ToolDef {
433 name: "reddb_kv_set",
434 command_id: "keyed.v1.kv",
435 description: "Set a key-value pair in a collection.",
436 input_schema: schema_with_nested(
437 vec![
438 ("collection", string_field("Collection name")),
439 ("key", string_field("Key to set")),
440 ("value", {
441 let mut f = Map::new();
442 f.insert("description".to_string(), JsonValue::String("Value to store (string, number, boolean, or null)".to_string()));
443 JsonValue::Object(f)
444 }),
445 ("tags", string_array_field("Optional KV invalidation tags")),
446 ("metadata", object_field("Optional metadata key/value pairs")),
447 ],
448 vec!["collection", "key", "value"],
449 ),
450 },
451 ToolDef {
452 name: "reddb_kv_invalidate_tags",
453 command_id: "keyed.v1.kv",
454 description: "Delete every KV entry in a collection tagged with any listed tag.",
455 input_schema: schema_with_nested(
456 vec![
457 ("collection", string_field("Collection name")),
458 ("tags", string_array_field("Tags to invalidate")),
459 ],
460 vec!["collection", "tags"],
461 ),
462 },
463 ToolDef {
464 name: "reddb_config_get",
465 command_id: "keyed.v1.config",
466 description: "Get a Config value without resolving SecretRef targets.",
467 input_schema: schema(
468 vec![
469 ("collection", "string", "Config collection name"),
470 ("key", "string", "Config key to retrieve"),
471 ],
472 vec!["collection", "key"],
473 ),
474 },
475 ToolDef {
476 name: "reddb_config_put",
477 command_id: "keyed.v1.config",
478 description: "Set a Config value. TTL and counter operations are not supported for Config.",
479 input_schema: schema_with_nested(
480 vec![
481 ("collection", string_field("Config collection name")),
482 ("key", string_field("Config key to set")),
483 ("value", {
484 let mut f = Map::new();
485 f.insert("description".to_string(), JsonValue::String("Value to store, or an object when paired with secret_ref".to_string()));
486 JsonValue::Object(f)
487 }),
488 ("secret_ref", object_field("Optional { collection, key } Vault SecretRef")),
489 ("tags", string_array_field("Optional Config tags")),
490 ],
491 vec!["collection", "key"],
492 ),
493 },
494 ToolDef {
495 name: "reddb_config_resolve",
496 command_id: "keyed.v1.config",
497 description: "Explicitly resolve a Config SecretRef. Requires the corresponding Vault unseal permission.",
498 input_schema: schema(
499 vec![
500 ("collection", "string", "Config collection name"),
501 ("key", "string", "Config key to resolve"),
502 ],
503 vec!["collection", "key"],
504 ),
505 },
506 ToolDef {
507 name: "reddb_vault_get",
508 command_id: "keyed.v1.vault",
509 description: "Get Vault metadata for a secret. Does not return plaintext.",
510 input_schema: schema(
511 vec![
512 ("collection", "string", "Vault collection name"),
513 ("key", "string", "Vault key to retrieve metadata for"),
514 ],
515 vec!["collection", "key"],
516 ),
517 },
518 ToolDef {
519 name: "reddb_vault_put",
520 command_id: "keyed.v1.vault",
521 description: "Store a sealed Vault secret. TTL and counter operations are not supported for Vault.",
522 input_schema: schema_with_nested(
523 vec![
524 ("collection", string_field("Vault collection name")),
525 ("key", string_field("Vault key to set")),
526 ("value", {
527 let mut f = Map::new();
528 f.insert("description".to_string(), JsonValue::String("Secret value to seal".to_string()));
529 JsonValue::Object(f)
530 }),
531 ("tags", string_array_field("Optional Vault tags")),
532 ],
533 vec!["collection", "key", "value"],
534 ),
535 },
536 ToolDef {
537 name: "reddb_vault_reveal",
538 command_id: "keyed.v1.vault",
539 description: "Explicitly reveal a Vault secret to a caller with vault:reveal permission.",
540 input_schema: schema(
541 vec![("collection", "string", "Vault collection name"), ("key", "string", "Vault key to reveal")],
542 vec!["collection", "key"],
543 ),
544 },
545 ToolDef {
546 name: "reddb_vault_unseal",
547 command_id: "keyed.v1.vault",
548 description: "Explicitly unseal a Vault secret and return plaintext to an authorized caller.",
549 input_schema: schema(
550 vec![
551 ("collection", "string", "Vault collection name"),
552 ("key", "string", "Vault key to unseal"),
553 ],
554 vec!["collection", "key"],
555 ),
556 },
557 ToolDef {
558 name: "reddb_delete",
559 command_id: "collections.entities.delete",
560 description: "Delete an entity by ID from a collection.",
561 input_schema: schema(
562 vec![
563 ("collection", "string", "Collection name"),
564 ("id", "integer", "Entity ID to delete"),
565 ],
566 vec!["collection", "id"],
567 ),
568 },
569 ToolDef {
570 name: "reddb_search_vector",
571 command_id: "collections.similar",
572 description: "Search for similar vectors using cosine similarity.",
573 input_schema: schema_with_nested(
574 vec![
575 ("collection", string_field("Collection to search in")),
576 ("vector", number_array_field("Query vector (array of floats)")),
577 ("k", integer_field("Number of results to return (default 10)")),
578 ("min_score", number_field("Minimum similarity score threshold (default 0.0)")),
579 ],
580 vec!["collection", "vector"],
581 ),
582 },
583 ToolDef {
584 name: "reddb_search_text",
585 command_id: "query.text_search",
586 description: "Full-text search across collections.",
587 input_schema: schema_with_nested(
588 vec![
589 ("query", string_field("Search query string")),
590 ("collections", {
591 let mut items = Map::new();
592 items.insert("type".to_string(), JsonValue::String("string".to_string()));
593 let mut f = Map::new();
594 f.insert("type".to_string(), JsonValue::String("array".to_string()));
595 f.insert("items".to_string(), JsonValue::Object(items));
596 f.insert("description".to_string(), JsonValue::String("Optional list of collections to search".to_string()));
597 JsonValue::Object(f)
598 }),
599 ("limit", integer_field("Maximum number of results (default 10)")),
600 ("fuzzy", boolean_field("Enable fuzzy matching (default false)")),
601 ],
602 vec!["query"],
603 ),
604 },
605 ToolDef {
606 name: "reddb_health",
607 command_id: "health.live",
608 description: "Check database health and return runtime statistics.",
609 input_schema: schema(vec![], vec![]),
610 },
611 ToolDef {
612 name: "reddb_graph_traverse",
613 command_id: "graph.traverse",
614 description: "Traverse the graph from a source node using BFS or DFS.",
615 input_schema: schema_with_nested(
616 vec![
617 ("source", string_field("Source node label to start traversal from")),
618 ("direction", string_field("Traversal direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
619 ("max_depth", integer_field("Maximum traversal depth (default 3)")),
620 ("strategy", string_field("Traversal strategy: 'bfs' or 'dfs' (default 'bfs')")),
621 ],
622 vec!["source"],
623 ),
624 },
625 ToolDef {
626 name: "reddb_graph_shortest_path",
627 command_id: "graph.shortest_path",
628 description: "Find the shortest path between two graph nodes.",
629 input_schema: schema_with_nested(
630 vec![
631 ("source", string_field("Source node label")),
632 ("target", string_field("Target node label")),
633 ("direction", string_field("Edge direction: 'outgoing', 'incoming', or 'both' (default 'outgoing')")),
634 (
635 "algorithm",
636 string_field(
637 "Path algorithm: 'bfs', 'dijkstra', 'astar', or 'bellman_ford' (default 'bfs')",
638 ),
639 ),
640 ],
641 vec!["source", "target"],
642 ),
643 },
644 ToolDef {
646 name: "reddb_auth_bootstrap",
647 command_id: "auth.bootstrap",
648 description: "Bootstrap the first admin user. Only works when no users exist yet. Returns the admin user and an API key.",
649 input_schema: schema(
650 vec![
651 ("username", "string", "Admin username"),
652 ("password", "string", "Admin password"),
653 ],
654 vec!["username", "password"],
655 ),
656 },
657 ToolDef {
658 name: "reddb_auth_create_user",
659 command_id: "auth.users.create",
660 description: "Create a new database user with a role (admin, write, or read).",
661 input_schema: schema(
662 vec![
663 ("username", "string", "Username for the new user"),
664 ("password", "string", "Password for the new user"),
665 ("role", "string", "Role: 'admin', 'write', or 'read'"),
666 ],
667 vec!["username", "password", "role"],
668 ),
669 },
670 ToolDef {
671 name: "reddb_auth_login",
672 command_id: "auth.login",
673 description: "Login with username and password. Returns a session token.",
674 input_schema: schema(
675 vec![
676 ("username", "string", "Username"),
677 ("password", "string", "Password"),
678 ],
679 vec!["username", "password"],
680 ),
681 },
682 ToolDef {
683 name: "reddb_auth_create_api_key",
684 command_id: "auth.api_keys.create",
685 description: "Create a persistent API key for a user.",
686 input_schema: schema(
687 vec![
688 ("username", "string", "Username to create the key for"),
689 ("name", "string", "Human-readable label for the key"),
690 ("role", "string", "Role for this key: 'admin', 'write', or 'read'"),
691 ],
692 vec!["username", "name", "role"],
693 ),
694 },
695 ToolDef {
696 name: "reddb_auth_list_users",
697 command_id: "auth.users.list",
698 description: "List all database users and their roles.",
699 input_schema: schema(vec![], vec![]),
700 },
701 ToolDef {
703 name: "reddb_update",
704 command_id: "collections.entities.patch",
705 description: "Update entities in a collection matching a filter.",
706 input_schema: schema_with_nested(
707 vec![
708 ("collection", string_field("Collection name")),
709 ("set", object_field("Key-value pairs to update")),
710 (
711 "where_filter",
712 string_field(
713 "Optional SQL WHERE clause (e.g., \"age > 21\")",
714 ),
715 ),
716 ],
717 vec!["collection", "set"],
718 ),
719 },
720 ToolDef {
721 name: "reddb_scan",
722 command_id: "collections.scan",
723 description: "Scan entities from a collection with pagination.",
724 input_schema: schema_with_nested(
725 vec![
726 ("collection", string_field("Collection to scan")),
727 ("limit", integer_field("Maximum number of results (default 10)")),
728 ("offset", integer_field("Number of records to skip (default 0)")),
729 ],
730 vec!["collection"],
731 ),
732 },
733 ToolDef {
735 name: "reddb_graph_centrality",
736 command_id: "graph.analytics.centrality",
737 description: "Compute centrality scores for graph nodes. Algorithms: degree, closeness, betweenness, eigenvector, pagerank.",
738 input_schema: schema_with_nested(
739 vec![(
740 "algorithm",
741 string_field(
742 "Centrality algorithm: 'degree', 'closeness', 'betweenness', 'eigenvector', 'pagerank'",
743 ),
744 )],
745 vec!["algorithm"],
746 ),
747 },
748 ToolDef {
749 name: "reddb_graph_community",
750 command_id: "graph.analytics.community",
751 description: "Detect communities in the graph. Algorithms: label_propagation, louvain.",
752 input_schema: schema_with_nested(
753 vec![
754 (
755 "algorithm",
756 string_field(
757 "Community detection algorithm: 'label_propagation' or 'louvain'",
758 ),
759 ),
760 (
761 "max_iterations",
762 integer_field("Maximum iterations (default 100)"),
763 ),
764 ],
765 vec!["algorithm"],
766 ),
767 },
768 ToolDef {
769 name: "reddb_graph_components",
770 command_id: "graph.analytics.components",
771 description: "Find connected components in the graph.",
772 input_schema: schema_with_nested(
773 vec![(
774 "mode",
775 string_field(
776 "Component mode: 'weakly_connected' or 'strongly_connected' (default 'weakly_connected')",
777 ),
778 )],
779 vec![],
780 ),
781 },
782 ToolDef {
783 name: "reddb_graph_cycles",
784 command_id: "graph.analytics.cycles",
785 description: "Detect cycles in the graph.",
786 input_schema: schema_with_nested(
787 vec![
788 (
789 "max_length",
790 integer_field("Maximum cycle length (default 10)"),
791 ),
792 (
793 "max_cycles",
794 integer_field("Maximum number of cycles to return (default 100)"),
795 ),
796 ],
797 vec![],
798 ),
799 },
800 ToolDef {
801 name: "reddb_graph_clustering",
802 command_id: "graph.analytics.clustering",
803 description: "Compute clustering coefficient for the graph.",
804 input_schema: schema(vec![], vec![]),
805 },
806 ToolDef {
808 name: "reddb_create_collection",
809 command_id: "collections.create",
810 description: "Create a new collection (table) in the database.",
811 input_schema: schema(
812 vec![("name", "string", "Collection name to create")],
813 vec!["name"],
814 ),
815 },
816 ToolDef {
817 name: "reddb_drop_collection",
818 command_id: "collections.drop",
819 description: "Drop (delete) a collection from the database.",
820 input_schema: schema(
821 vec![("name", "string", "Collection name to drop")],
822 vec!["name"],
823 ),
824 },
825 ToolDef {
829 name: "reddb_rql_validate",
830 command_id: "query.contract",
831 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.",
832 input_schema: schema(
833 vec![("rql", "string", "RQL query string to validate")],
834 vec!["rql"],
835 ),
836 },
837 ToolDef {
838 name: "reddb_rql_explain",
839 command_id: "query.explain",
840 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.",
841 input_schema: schema(
842 vec![("rql", "string", "RQL query string to parse and explain")],
843 vec!["rql"],
844 ),
845 },
846 ToolDef {
849 name: "reddb_type_of",
850 command_id: "catalog.snapshot",
851 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.",
852 input_schema: schema_with_nested(
853 vec![
854 ("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.")),
855 ("value", {
856 let mut f = Map::new();
857 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()));
858 f.insert(
859 "anyOf".to_string(),
860 JsonValue::Array(vec![
861 type_field("null"),
862 type_field("boolean"),
863 type_field("number"),
864 type_field("string"),
865 type_field("array"),
866 type_field("object"),
867 ]),
868 );
869 JsonValue::Object(f)
870 }),
871 ],
872 vec![],
873 ),
874 },
875 ToolDef {
879 name: "reddb_explain_connection",
880 command_id: "ops.capabilities",
881 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.",
882 input_schema: schema(
883 vec![(
884 "uri",
885 "string",
886 "Connection URI to explain, e.g. memory://, file:///data.rdb, reds://host, grpc://host, or https://host",
887 )],
888 vec!["uri"],
889 ),
890 },
891 ]);
892 tools
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898
899 #[test]
900 fn embedded_identity_authorizes_the_full_tool_catalog() {
901 for tool in all_tools() {
902 authorize_tool(&tool.name, McpIdentityPosture::EmbeddedImplicitAdmin)
903 .unwrap_or_else(|err| panic!("embedded tool {} was denied: {err}", tool.name));
904 }
905 authorize_tool(
906 crate::runtime::ai::mcp_ask_tool::TOOL_NAME,
907 McpIdentityPosture::EmbeddedImplicitAdmin,
908 )
909 .expect("embedded reddb.ask is authorized");
910 }
911
912 #[test]
913 fn tool_descriptors_document_catalog_authorization_posture() {
914 for tool in all_tools() {
915 let description = tool.descriptor_description();
916 assert!(
917 description.contains("Embedded/local mode runs as implicit admin"),
918 "embedded posture missing from {}: {}",
919 tool.name,
920 description
921 );
922 assert!(
923 description.contains("Remote mode requires credentials"),
924 "remote posture missing from {}: {}",
925 tool.name,
926 description
927 );
928 assert!(
929 description.contains(tool.command_id),
930 "canonical command id {} missing from {} descriptor",
931 tool.command_id,
932 tool.name
933 );
934 }
935
936 let ask = ask_descriptor();
937 let ask_description = ask
938 .get("description")
939 .and_then(JsonValue::as_str)
940 .expect("reddb.ask description");
941 assert!(ask_description.contains("Embedded/local mode runs as implicit admin"));
942 assert!(ask_description.contains("Remote mode requires credentials"));
943 assert!(ask_description.contains("ai.ask"));
944 }
945
946 #[test]
947 fn test_all_tools_defined() {
948 let tools = all_tools();
949 assert!(tools.len() >= 24);
950 let names: Vec<&str> = tools.iter().map(|t| t.name).collect();
951 assert!(names.contains(&"reddb_query"));
952 assert!(names.contains(&"reddb_collections"));
953 assert!(names.contains(&"reddb_insert_row"));
954 assert!(names.contains(&"reddb_insert_node"));
955 assert!(names.contains(&"reddb_insert_edge"));
956 assert!(names.contains(&"reddb_insert_vector"));
957 assert!(names.contains(&"reddb_insert_document"));
958 assert!(names.contains(&"reddb_kv_get"));
959 assert!(names.contains(&"reddb_kv_set"));
960 assert!(names.contains(&"reddb_config_get"));
961 assert!(names.contains(&"reddb_config_put"));
962 assert!(names.contains(&"reddb_config_resolve"));
963 assert!(names.contains(&"reddb_vault_get"));
964 assert!(names.contains(&"reddb_vault_put"));
965 assert!(names.contains(&"reddb_vault_reveal"));
966 assert!(names.contains(&"reddb_vault_unseal"));
967 assert!(names.contains(&"reddb_delete"));
968 assert!(names.contains(&"reddb_search_vector"));
969 assert!(names.contains(&"reddb_search_text"));
970 assert!(names.contains(&"reddb_health"));
971 assert!(names.contains(&"reddb_graph_traverse"));
972 assert!(names.contains(&"reddb_graph_shortest_path"));
973 assert!(names.contains(&"reddb_update"));
975 assert!(names.contains(&"reddb_scan"));
976 assert!(names.contains(&"reddb_graph_centrality"));
977 assert!(names.contains(&"reddb_graph_community"));
978 assert!(names.contains(&"reddb_graph_components"));
979 assert!(names.contains(&"reddb_graph_cycles"));
980 assert!(names.contains(&"reddb_graph_clustering"));
981 assert!(names.contains(&"reddb_create_collection"));
982 assert!(names.contains(&"reddb_drop_collection"));
983 assert!(names.contains(&"reddb_rql_validate"));
985 assert!(names.contains(&"reddb_rql_explain"));
986 assert!(names.contains(&"reddb_type_of"));
988 assert!(names.contains(&"reddb_explain_connection"));
990 }
991
992 #[test]
993 fn test_rql_validate_tool_schema() {
994 let tools = all_tools();
995 let tool = tools
996 .iter()
997 .find(|t| t.name == "reddb_rql_validate")
998 .expect("reddb_rql_validate registered");
999 let props = tool
1000 .input_schema
1001 .get("properties")
1002 .and_then(|v| v.as_object())
1003 .unwrap();
1004 assert!(props.contains_key("rql"));
1005 let required = tool
1006 .input_schema
1007 .get("required")
1008 .and_then(|v| v.as_array())
1009 .unwrap();
1010 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1011 assert!(required_strs.contains(&"rql"));
1012 }
1013
1014 #[test]
1015 fn test_rql_explain_tool_schema() {
1016 let tools = all_tools();
1017 let tool = tools
1018 .iter()
1019 .find(|t| t.name == "reddb_rql_explain")
1020 .expect("reddb_rql_explain registered");
1021 let props = tool
1022 .input_schema
1023 .get("properties")
1024 .and_then(|v| v.as_object())
1025 .unwrap();
1026 assert!(props.contains_key("rql"));
1027 let required = tool
1028 .input_schema
1029 .get("required")
1030 .and_then(|v| v.as_array())
1031 .unwrap();
1032 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1033 assert!(required_strs.contains(&"rql"));
1034 }
1035
1036 #[test]
1037 fn test_tool_schemas_have_type() {
1038 for tool in all_tools() {
1039 assert_eq!(
1040 tool.input_schema.get("type").and_then(|v| v.as_str()),
1041 Some("object"),
1042 "tool '{}' schema must have type=object",
1043 tool.name,
1044 );
1045 }
1046 }
1047
1048 #[test]
1049 fn test_query_tool_schema_exposes_optional_params_array() {
1050 let tools = all_tools();
1051 let tool = tools.iter().find(|t| t.name == "reddb_query").unwrap();
1052 let props = tool
1053 .input_schema
1054 .get("properties")
1055 .and_then(|v| v.as_object())
1056 .unwrap();
1057 let params = props.get("params").and_then(|v| v.as_object()).unwrap();
1058 assert_eq!(params.get("type").and_then(|v| v.as_str()), Some("array"));
1059 let accepted_param_types = params
1060 .get("items")
1061 .and_then(|v| v.get("anyOf"))
1062 .and_then(|v| v.as_array())
1063 .expect("query params enumerate the supported JSON value types");
1064 assert_eq!(accepted_param_types.len(), 6);
1065
1066 let required = tool
1067 .input_schema
1068 .get("required")
1069 .and_then(|v| v.as_array())
1070 .unwrap();
1071 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1072 assert!(required_strs.contains(&"sql"));
1073 assert!(!required_strs.contains(&"params"));
1074 }
1075
1076 #[test]
1077 fn test_update_tool_schema() {
1078 let tools = all_tools();
1079 let tool = tools.iter().find(|t| t.name == "reddb_update").unwrap();
1080 assert_eq!(tool.name, "reddb_update");
1081 let props = tool
1082 .input_schema
1083 .get("properties")
1084 .and_then(|v| v.as_object())
1085 .unwrap();
1086 assert!(props.contains_key("collection"));
1087 assert!(props.contains_key("set"));
1088 assert!(props.contains_key("where_filter"));
1089 let required = tool
1090 .input_schema
1091 .get("required")
1092 .and_then(|v| v.as_array())
1093 .unwrap();
1094 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1095 assert!(required_strs.contains(&"collection"));
1096 assert!(required_strs.contains(&"set"));
1097 assert!(!required_strs.contains(&"where_filter"));
1098 }
1099
1100 #[test]
1101 fn test_scan_tool_schema() {
1102 let tools = all_tools();
1103 let tool = tools.iter().find(|t| t.name == "reddb_scan").unwrap();
1104 let props = tool
1105 .input_schema
1106 .get("properties")
1107 .and_then(|v| v.as_object())
1108 .unwrap();
1109 assert!(props.contains_key("collection"));
1110 assert!(props.contains_key("limit"));
1111 assert!(props.contains_key("offset"));
1112 let required = tool
1113 .input_schema
1114 .get("required")
1115 .and_then(|v| v.as_array())
1116 .unwrap();
1117 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1118 assert!(required_strs.contains(&"collection"));
1119 assert!(!required_strs.contains(&"limit"));
1121 assert!(!required_strs.contains(&"offset"));
1122 }
1123
1124 #[test]
1125 fn test_graph_centrality_tool_schema() {
1126 let tools = all_tools();
1127 let tool = tools
1128 .iter()
1129 .find(|t| t.name == "reddb_graph_centrality")
1130 .unwrap();
1131 let props = tool
1132 .input_schema
1133 .get("properties")
1134 .and_then(|v| v.as_object())
1135 .unwrap();
1136 assert!(props.contains_key("algorithm"));
1137 let required = tool
1138 .input_schema
1139 .get("required")
1140 .and_then(|v| v.as_array())
1141 .unwrap();
1142 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1143 assert!(required_strs.contains(&"algorithm"));
1144 }
1145
1146 #[test]
1147 fn test_graph_community_tool_schema() {
1148 let tools = all_tools();
1149 let tool = tools
1150 .iter()
1151 .find(|t| t.name == "reddb_graph_community")
1152 .unwrap();
1153 let props = tool
1154 .input_schema
1155 .get("properties")
1156 .and_then(|v| v.as_object())
1157 .unwrap();
1158 assert!(props.contains_key("algorithm"));
1159 assert!(props.contains_key("max_iterations"));
1160 let required = tool
1161 .input_schema
1162 .get("required")
1163 .and_then(|v| v.as_array())
1164 .unwrap();
1165 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1166 assert!(required_strs.contains(&"algorithm"));
1167 assert!(!required_strs.contains(&"max_iterations"));
1168 }
1169
1170 #[test]
1171 fn test_graph_components_tool_schema() {
1172 let tools = all_tools();
1173 let tool = tools
1174 .iter()
1175 .find(|t| t.name == "reddb_graph_components")
1176 .unwrap();
1177 let props = tool
1178 .input_schema
1179 .get("properties")
1180 .and_then(|v| v.as_object())
1181 .unwrap();
1182 assert!(props.contains_key("mode"));
1183 let required = tool
1184 .input_schema
1185 .get("required")
1186 .and_then(|v| v.as_array())
1187 .unwrap();
1188 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1190 assert!(required_strs.is_empty());
1191 }
1192
1193 #[test]
1194 fn test_graph_cycles_tool_schema() {
1195 let tools = all_tools();
1196 let tool = tools
1197 .iter()
1198 .find(|t| t.name == "reddb_graph_cycles")
1199 .unwrap();
1200 let props = tool
1201 .input_schema
1202 .get("properties")
1203 .and_then(|v| v.as_object())
1204 .unwrap();
1205 assert!(props.contains_key("max_length"));
1206 assert!(props.contains_key("max_cycles"));
1207 let required = tool
1208 .input_schema
1209 .get("required")
1210 .and_then(|v| v.as_array())
1211 .unwrap();
1212 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1214 assert!(required_strs.is_empty());
1215 }
1216
1217 #[test]
1218 fn test_graph_clustering_tool_schema() {
1219 let tools = all_tools();
1220 let tool = tools
1221 .iter()
1222 .find(|t| t.name == "reddb_graph_clustering")
1223 .unwrap();
1224 let props = tool
1225 .input_schema
1226 .get("properties")
1227 .and_then(|v| v.as_object())
1228 .unwrap();
1229 assert!(props.is_empty());
1231 }
1232
1233 #[test]
1234 fn test_create_collection_tool_schema() {
1235 let tools = all_tools();
1236 let tool = tools
1237 .iter()
1238 .find(|t| t.name == "reddb_create_collection")
1239 .unwrap();
1240 let props = tool
1241 .input_schema
1242 .get("properties")
1243 .and_then(|v| v.as_object())
1244 .unwrap();
1245 assert!(props.contains_key("name"));
1246 let name_type = props
1247 .get("name")
1248 .and_then(|v| v.get("type"))
1249 .and_then(|v| v.as_str());
1250 assert_eq!(name_type, Some("string"));
1251 let required = tool
1252 .input_schema
1253 .get("required")
1254 .and_then(|v| v.as_array())
1255 .unwrap();
1256 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1257 assert!(required_strs.contains(&"name"));
1258 }
1259
1260 #[test]
1261 fn test_drop_collection_tool_schema() {
1262 let tools = all_tools();
1263 let tool = tools
1264 .iter()
1265 .find(|t| t.name == "reddb_drop_collection")
1266 .unwrap();
1267 let props = tool
1268 .input_schema
1269 .get("properties")
1270 .and_then(|v| v.as_object())
1271 .unwrap();
1272 assert!(props.contains_key("name"));
1273 let required = tool
1274 .input_schema
1275 .get("required")
1276 .and_then(|v| v.as_array())
1277 .unwrap();
1278 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
1279 assert!(required_strs.contains(&"name"));
1280 }
1281
1282 #[test]
1283 fn test_type_of_tool_schema() {
1284 let tools = all_tools();
1285 let tool = tools.iter().find(|t| t.name == "reddb_type_of").unwrap();
1286 let props = tool
1287 .input_schema
1288 .get("properties")
1289 .and_then(|v| v.as_object())
1290 .unwrap();
1291 assert!(props.contains_key("type"));
1294 assert!(props.contains_key("value"));
1295 let required = tool
1296 .input_schema
1297 .get("required")
1298 .and_then(|v| v.as_array())
1299 .unwrap();
1300 assert!(required.is_empty());
1301 }
1302}