1use crate::application::{
7 CreateDocumentInput, CreateEdgeInput, CreateNodeInput, CreateRowInput, CreateVectorInput,
8 DeleteEntityInput, EntityUseCases, ExecuteQueryInput, GraphCentralityInput,
9 GraphClusteringInput, GraphCommunitiesInput, GraphComponentsInput, GraphCyclesInput,
10 GraphShortestPathInput, GraphTraversalInput, GraphUseCases, ScanCollectionInput,
11 SearchSimilarInput, SearchTextInput,
12};
13use crate::auth::store::AuthStore;
14use crate::auth::{AuthConfig, Role};
15use crate::json::{
16 from_str as json_from_str, to_string as json_to_string, Map, Value as JsonValue,
17};
18use crate::mcp::capability::McpCapability;
19use crate::mcp::{protocol, tools};
20use crate::presentation::entity_json::created_entity_output_json;
21use crate::presentation::entity_json::storage_value_to_json;
22use crate::presentation::query_result_json::{runtime_query_json, runtime_stats_json};
23use crate::runtime::{
24 query_request::ParamValue, RedDBRuntime, RuntimeGraphCentralityAlgorithm,
25 RuntimeGraphCommunityAlgorithm, RuntimeGraphComponentsMode, RuntimeGraphDirection,
26 RuntimeGraphPathAlgorithm, RuntimeGraphTraversalStrategy,
27};
28use crate::storage::EntityId;
29use reddb_types::Value;
30
31use std::io::{self, BufRead, Write};
32use std::sync::Arc;
33
34pub struct McpServer {
36 runtime: RedDBRuntime,
37 auth_store: Arc<AuthStore>,
38 initialized: bool,
39 capability: McpCapability,
43}
44
45impl McpServer {
46 pub fn new(runtime: RedDBRuntime) -> Self {
48 let auth_store = Arc::new(AuthStore::new(AuthConfig {
49 enabled: true,
50 ..Default::default()
51 }));
52 auth_store.bootstrap_from_env();
53 runtime.set_auth_store(Arc::clone(&auth_store));
54 Self {
55 runtime,
56 auth_store,
57 initialized: false,
58 capability: McpCapability::default(),
59 }
60 }
61
62 pub fn with_capability(mut self, capability: McpCapability) -> Self {
65 self.capability = capability;
66 self
67 }
68
69 pub fn capability(&self) -> McpCapability {
71 self.capability
72 }
73
74 fn check_tool_capability(&self, tool_name: &str) -> Result<(), String> {
76 let required = crate::mcp::capability::tool_capability(tool_name);
77 if self.capability.permits(required) {
78 Ok(())
79 } else {
80 Err(crate::mcp::capability::refusal(
81 &format!("tool `{tool_name}`"),
82 self.capability,
83 required,
84 ))
85 }
86 }
87
88 pub fn run_stdio(&mut self) {
93 let stdin = io::stdin();
94 let stdout = io::stdout();
95 let mut reader = io::BufReader::new(stdin.lock());
96 let mut writer = io::BufWriter::new(stdout.lock());
97
98 tracing::info!(target: "reddb::mcp", "server started, waiting for messages on stdin");
99
100 loop {
101 let payload = match protocol::read_payload(&mut reader) {
102 Ok(Some(p)) => p,
103 Ok(None) => {
104 tracing::info!(target: "reddb::mcp", "stdin closed, shutting down");
105 break;
106 }
107 Err(e) => {
108 tracing::error!(target: "reddb::mcp", err = %e, "read error");
109 continue;
110 }
111 };
112
113 let request: JsonValue = match json_from_str(&payload) {
114 Ok(v) => v,
115 Err(e) => {
116 tracing::warn!(target: "reddb::mcp", err = %e, "invalid JSON");
117 let msg = protocol::build_error_message(None, -32700, "parse error");
118 let _ = protocol::write_message(&mut writer, &msg);
119 continue;
120 }
121 };
122
123 let response = self.handle_message(&request);
124 if let Some(resp) = response {
125 if let Err(e) = protocol::write_message(&mut writer, &resp) {
126 tracing::error!(target: "reddb::mcp", err = %e, "write error");
127 break;
128 }
129 }
130 }
131 }
132
133 fn handle_message(&mut self, msg: &JsonValue) -> Option<String> {
135 let method = msg.get("method").and_then(|v| v.as_str())?;
136 let id = msg.get("id");
137
138 match method {
139 "initialize" => Some(self.handle_initialize(id)),
140 "initialized" | "notifications/initialized" => {
141 None
143 }
144 "tools/list" => Some(self.handle_tools_list(id)),
145 "tools/call" => Some(self.handle_tools_call(id, msg.get("params"))),
146 "resources/list" => Some(self.handle_resources_list(id)),
147 "resources/read" => Some(self.handle_resources_read(id, msg.get("params"))),
148 "ping" => {
149 let mut result = Map::new();
150 result.insert("status".to_string(), JsonValue::String("ok".to_string()));
151 Some(protocol::build_result_message(
152 id,
153 JsonValue::Object(result),
154 ))
155 }
156 _ => Some(protocol::build_error_message(
157 id,
158 -32601,
159 &format!("unknown method: {}", method),
160 )),
161 }
162 }
163
164 fn handle_initialize(&mut self, id: Option<&JsonValue>) -> String {
169 self.initialized = true;
170
171 let mut capabilities = Map::new();
172 {
173 let mut tools_cap = Map::new();
174 tools_cap.insert("listChanged".to_string(), JsonValue::Bool(false));
175 capabilities.insert("tools".to_string(), JsonValue::Object(tools_cap));
176 }
177 {
178 let mut resources_cap = Map::new();
181 resources_cap.insert("subscribe".to_string(), JsonValue::Bool(false));
182 resources_cap.insert("listChanged".to_string(), JsonValue::Bool(false));
183 capabilities.insert("resources".to_string(), JsonValue::Object(resources_cap));
184 }
185
186 let mut server_info = Map::new();
187 server_info.insert(
188 "name".to_string(),
189 JsonValue::String("reddb-mcp".to_string()),
190 );
191 server_info.insert(
192 "version".to_string(),
193 JsonValue::String(env!("CARGO_PKG_VERSION").to_string()),
194 );
195
196 let mut result = Map::new();
197 result.insert(
198 "protocolVersion".to_string(),
199 JsonValue::String("2024-11-05".to_string()),
200 );
201 result.insert("capabilities".to_string(), JsonValue::Object(capabilities));
202 result.insert("serverInfo".to_string(), JsonValue::Object(server_info));
203
204 protocol::build_result_message(id, JsonValue::Object(result))
205 }
206
207 fn handle_tools_list(&self, id: Option<&JsonValue>) -> String {
212 let defs = tools::all_tools();
213 let mut tools_json: Vec<JsonValue> = defs
214 .into_iter()
215 .map(|def| {
216 let mut obj = Map::new();
217 obj.insert("name".to_string(), JsonValue::String(def.name.to_string()));
218 obj.insert(
219 "description".to_string(),
220 JsonValue::String(def.descriptor_description()),
221 );
222 obj.insert("inputSchema".to_string(), def.input_schema);
223 JsonValue::Object(obj)
224 })
225 .collect();
226 tools_json.push(tools::ask_descriptor());
227
228 let mut result = Map::new();
229 result.insert("tools".to_string(), JsonValue::Array(tools_json));
230 protocol::build_result_message(id, JsonValue::Object(result))
231 }
232
233 fn handle_resources_list(&self, id: Option<&JsonValue>) -> String {
238 let resources: Vec<JsonValue> = tools::knowledge_resources()
239 .iter()
240 .map(|res| {
241 let mut obj = Map::new();
242 obj.insert("uri".to_string(), JsonValue::String(res.uri.to_string()));
243 obj.insert("name".to_string(), JsonValue::String(res.title.to_string()));
244 obj.insert(
245 "description".to_string(),
246 JsonValue::String(res.description.to_string()),
247 );
248 obj.insert(
249 "mimeType".to_string(),
250 JsonValue::String(res.mime_type.to_string()),
251 );
252 JsonValue::Object(obj)
253 })
254 .collect();
255
256 let mut result = Map::new();
257 result.insert("resources".to_string(), JsonValue::Array(resources));
258 protocol::build_result_message(id, JsonValue::Object(result))
259 }
260
261 fn handle_resources_read(&self, id: Option<&JsonValue>, params: Option<&JsonValue>) -> String {
262 let uri = params.and_then(|p| p.get("uri")).and_then(|v| v.as_str());
263 let uri = match uri {
264 Some(u) => u,
265 None => return protocol::build_error_message(id, -32602, "missing resource uri"),
266 };
267
268 let resources = tools::knowledge_resources();
269 let resource = match resources.iter().find(|res| res.uri == uri) {
270 Some(res) => res,
271 None => {
272 return protocol::build_error_message(
273 id,
274 -32602,
275 &format!("unknown resource: {uri}"),
276 );
277 }
278 };
279
280 let mut contents = Map::new();
281 contents.insert(
282 "uri".to_string(),
283 JsonValue::String(resource.uri.to_string()),
284 );
285 contents.insert(
286 "mimeType".to_string(),
287 JsonValue::String(resource.mime_type.to_string()),
288 );
289 contents.insert("text".to_string(), JsonValue::String((resource.body)()));
290
291 let mut result = Map::new();
292 result.insert(
293 "contents".to_string(),
294 JsonValue::Array(vec![JsonValue::Object(contents)]),
295 );
296 protocol::build_result_message(id, JsonValue::Object(result))
297 }
298
299 fn handle_tools_call(&self, id: Option<&JsonValue>, params: Option<&JsonValue>) -> String {
304 let name = params.and_then(|p| p.get("name")).and_then(|v| v.as_str());
305 let name = match name {
306 Some(n) => n,
307 None => {
308 return protocol::build_error_message(id, -32602, "missing tool name");
309 }
310 };
311
312 let empty = JsonValue::Object(Map::new());
313 let args = params.and_then(|p| p.get("arguments")).unwrap_or(&empty);
314
315 let result = tools::authorize_tool(name, tools::McpIdentityPosture::EmbeddedImplicitAdmin)
316 .and_then(|_| self.check_tool_capability(name))
317 .and_then(|_| match name {
318 "reddb_query" => self.tool_query(args),
319 "reddb_collections" => self.tool_collections(),
320 "reddb_insert_row" => self.tool_insert_row(args),
321 "reddb_insert_node" => self.tool_insert_node(args),
322 "reddb_insert_edge" => self.tool_insert_edge(args),
323 "reddb_insert_vector" => self.tool_insert_vector(args),
324 "reddb_insert_document" => self.tool_insert_document(args),
325 "reddb_kv_get" => self.tool_kv_get(args),
326 "reddb_kv_set" => self.tool_kv_set(args),
327 "reddb_kv_invalidate_tags" => self.tool_kv_invalidate_tags(args),
328 "reddb_config_get" => self.tool_config_get(args),
329 "reddb_config_put" => self.tool_config_put(args),
330 "reddb_config_resolve" => self.tool_config_resolve(args),
331 "reddb_vault_get" => self.tool_vault_get(args),
332 "reddb_vault_put" => self.tool_vault_put(args),
333 "reddb_vault_reveal" | "reddb_vault_unseal" => self.tool_vault_unseal(args),
334 "reddb_delete" => self.tool_delete(args),
335 "reddb_search_vector" => self.tool_search_vector(args),
336 "reddb_search_text" => self.tool_search_text(args),
337 "reddb_health" => self.tool_health(),
338 "reddb_graph_traverse" => self.tool_graph_traverse(args),
339 "reddb_graph_shortest_path" => self.tool_graph_shortest_path(args),
340 "reddb_update" => self.tool_update(args),
341 "reddb_scan" => self.tool_scan(args),
342 "reddb_graph_centrality" => self.tool_graph_centrality(args),
343 "reddb_graph_community" => self.tool_graph_community(args),
344 "reddb_graph_components" => self.tool_graph_components(args),
345 "reddb_graph_cycles" => self.tool_graph_cycles(args),
346 "reddb_graph_clustering" => self.tool_graph_clustering(args),
347 "reddb_create_collection" => self.tool_create_collection(args),
348 "reddb_drop_collection" => self.tool_drop_collection(args),
349 "reddb_rql_validate" => self.tool_rql_validate(args),
350 "reddb_rql_explain" => self.tool_rql_explain(args),
351 "reddb_type_of" => self.tool_type_of(args),
352 "reddb_explain_connection" => self.tool_explain_connection(args),
353 "reddb_auth_bootstrap" => self.tool_auth_bootstrap(args),
354 "reddb_auth_create_user" => self.tool_auth_create_user(args),
355 "reddb_auth_login" => self.tool_auth_login(args),
356 "reddb_auth_create_api_key" => self.tool_auth_create_api_key(args),
357 "reddb_auth_list_users" => self.tool_auth_list_users(),
358 crate::runtime::ai::mcp_ask_tool::TOOL_NAME => self.tool_ask(args),
359 _ => Err(format!("unknown tool: {name}")),
360 });
361
362 match result {
363 Ok(text) => {
364 let mut content = Map::new();
365 content.insert("type".to_string(), JsonValue::String("text".to_string()));
366 content.insert("text".to_string(), JsonValue::String(text));
367
368 let mut result_obj = Map::new();
369 result_obj.insert(
370 "content".to_string(),
371 JsonValue::Array(vec![JsonValue::Object(content)]),
372 );
373 protocol::build_result_message(id, JsonValue::Object(result_obj))
374 }
375 Err(err) => {
376 let mut content = Map::new();
377 content.insert("type".to_string(), JsonValue::String("text".to_string()));
378 content.insert("text".to_string(), JsonValue::String(err.clone()));
379
380 let mut result_obj = Map::new();
381 result_obj.insert(
382 "content".to_string(),
383 JsonValue::Array(vec![JsonValue::Object(content)]),
384 );
385 result_obj.insert("isError".to_string(), JsonValue::Bool(true));
386 protocol::build_result_message(id, JsonValue::Object(result_obj))
387 }
388 }
389 }
390
391 fn tool_query(&self, args: &JsonValue) -> Result<String, String> {
396 let sql = args
397 .get("sql")
398 .and_then(|v| v.as_str())
399 .ok_or("missing required field 'sql'")?;
400
401 let required = crate::mcp::capability::statement_capability(sql);
406 if !self.capability.permits(required) {
407 return Err(crate::mcp::capability::refusal(
408 "this statement",
409 self.capability,
410 required,
411 ));
412 }
413
414 if let Some(raw_params) = args.get("params") {
417 let arr = raw_params
418 .as_array()
419 .ok_or_else(|| "'params' must be an array".to_string())?;
420 let binds: Vec<Value> = arr
421 .iter()
422 .map(ParamValue::decode_json)
423 .collect::<Result<Vec<_>, _>>()?
424 .into_iter()
425 .map(Value::from)
426 .collect();
427
428 let result = self
432 .runtime
433 .execute_query_with_params(sql, &binds)
434 .map_err(|e| format!("{}", e))?;
435 let json = runtime_query_json(&result, &None, &None);
436 return json_to_string(&json).map_err(|e| format!("serialization error: {}", e));
437 }
438
439 let uc = &self.runtime;
440 let result = uc.execute_query(sql).map_err(|e| format!("{}", e))?;
441
442 let json = runtime_query_json(&result, &None, &None);
443 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
444 }
445
446 fn tool_ask(&self, args: &JsonValue) -> Result<String, String> {
447 let invocation =
448 crate::runtime::ai::mcp_ask_tool::parse(args).map_err(format_mcp_ask_parse_error)?;
449 let ask = reddb_rql::ast::AskQuery {
450 explain: false,
451 question: invocation.question,
452 question_param: None,
453 provider: invocation.using,
454 model: invocation.model,
455 depth: invocation.depth.map(|v| v as usize),
456 limit: invocation.limit.map(|v| v as usize),
457 min_score: invocation.min_score.map(|v| v as f32),
458 collection: None,
459 temperature: invocation.temperature.map(|v| v as f32),
460 seed: invocation.seed,
461 strict: invocation.strict.unwrap_or(true),
462 stream: false,
463 cache: if matches!(invocation.nocache, Some(true)) {
464 reddb_rql::ast::AskCacheClause::NoCache
465 } else if let Some(ttl) = invocation.cache_ttl {
466 reddb_rql::ast::AskCacheClause::CacheTtl(ttl)
467 } else {
468 reddb_rql::ast::AskCacheClause::Default
469 },
470 plan_only: false,
471 steps: None,
472 };
473 let result = self
474 .runtime
475 .execute_ask("ASK <mcp>", &ask)
476 .map_err(|e| format!("{}", e))?;
477 let json = crate::presentation::query_result::tagged_summary(&result);
478 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
479 }
480
481 fn tool_collections(&self) -> Result<String, String> {
482 let uc = &self.runtime;
483 let collections = uc.db().collections();
484 let json = JsonValue::Array(collections.into_iter().map(JsonValue::String).collect());
485 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
486 }
487
488 fn tool_insert_row(&self, args: &JsonValue) -> Result<String, String> {
489 let collection = args
490 .get("collection")
491 .and_then(|v| v.as_str())
492 .ok_or("missing required field 'collection'")?;
493 let data = args
494 .get("data")
495 .and_then(|v| v.as_object())
496 .ok_or("missing required field 'data' (must be an object)")?;
497
498 let mut fields = Vec::new();
499 for (key, value) in data {
500 let sv = crate::application::entity::json_to_storage_value(value)
501 .map_err(|e| format!("{}", e))?;
502 fields.push((key.clone(), sv));
503 }
504
505 let metadata = parse_metadata_arg(args)?;
506
507 let uc = EntityUseCases::new(&self.runtime);
508 let output = uc
509 .create_row(CreateRowInput {
510 collection: collection.to_string(),
511 fields,
512 metadata,
513 node_links: vec![],
514 vector_links: vec![],
515 })
516 .map_err(|e| format!("{}", e))?;
517
518 let json = created_entity_output_json(&output);
519 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
520 }
521
522 fn tool_insert_node(&self, args: &JsonValue) -> Result<String, String> {
523 let collection = args
524 .get("collection")
525 .and_then(|v| v.as_str())
526 .ok_or("missing required field 'collection'")?;
527 let label = args
528 .get("label")
529 .and_then(|v| v.as_str())
530 .ok_or("missing required field 'label'")?;
531 let node_type = args
532 .get("node_type")
533 .and_then(|v| v.as_str())
534 .map(String::from);
535
536 let mut properties = Vec::new();
537 if let Some(props) = args.get("properties").and_then(|v| v.as_object()) {
538 for (key, value) in props {
539 let sv = crate::application::entity::json_to_storage_value(value)
540 .map_err(|e| format!("{}", e))?;
541 properties.push((key.clone(), sv));
542 }
543 }
544
545 let metadata = parse_metadata_arg(args)?;
546
547 let uc = EntityUseCases::new(&self.runtime);
548 let output = uc
549 .create_node(CreateNodeInput {
550 collection: collection.to_string(),
551 label: label.to_string(),
552 node_type,
553 properties,
554 metadata,
555 embeddings: vec![],
556 table_links: vec![],
557 node_links: vec![],
558 })
559 .map_err(|e| format!("{}", e))?;
560
561 let json = created_entity_output_json(&output);
562 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
563 }
564
565 fn tool_insert_edge(&self, args: &JsonValue) -> Result<String, String> {
566 let collection = args
567 .get("collection")
568 .and_then(|v| v.as_str())
569 .ok_or("missing required field 'collection'")?;
570 let label = args
571 .get("label")
572 .and_then(|v| v.as_str())
573 .ok_or("missing required field 'label'")?;
574 let from_id = args
575 .get("from")
576 .and_then(|v| v.as_u64())
577 .ok_or("missing required field 'from' (integer)")?;
578 let to_id = args
579 .get("to")
580 .and_then(|v| v.as_u64())
581 .ok_or("missing required field 'to' (integer)")?;
582 let weight = args
583 .get("weight")
584 .and_then(|v| v.as_f64())
585 .map(|w| w as f32);
586
587 let mut properties = Vec::new();
588 if let Some(props) = args.get("properties").and_then(|v| v.as_object()) {
589 for (key, value) in props {
590 let sv = crate::application::entity::json_to_storage_value(value)
591 .map_err(|e| format!("{}", e))?;
592 properties.push((key.clone(), sv));
593 }
594 }
595
596 let metadata = parse_metadata_arg(args)?;
597
598 let uc = EntityUseCases::new(&self.runtime);
599 let output = uc
600 .create_edge(CreateEdgeInput {
601 collection: collection.to_string(),
602 label: label.to_string(),
603 from: EntityId::new(from_id),
604 to: EntityId::new(to_id),
605 weight,
606 properties,
607 metadata,
608 })
609 .map_err(|e| format!("{}", e))?;
610
611 let json = created_entity_output_json(&output);
612 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
613 }
614
615 fn tool_insert_vector(&self, args: &JsonValue) -> Result<String, String> {
616 let collection = args
617 .get("collection")
618 .and_then(|v| v.as_str())
619 .ok_or("missing required field 'collection'")?;
620 let dense_arr = args
621 .get("dense")
622 .and_then(|v| v.as_array())
623 .ok_or("missing required field 'dense' (array of numbers)")?;
624
625 let mut dense = Vec::with_capacity(dense_arr.len());
626 for v in dense_arr {
627 dense.push(
628 v.as_f64()
629 .ok_or("'dense' array must contain only numbers")? as f32,
630 );
631 }
632 if dense.is_empty() {
633 return Err("'dense' vector cannot be empty".to_string());
634 }
635
636 let content = args
637 .get("content")
638 .and_then(|v| v.as_str())
639 .map(String::from);
640 let metadata = parse_metadata_arg(args)?;
641
642 let uc = EntityUseCases::new(&self.runtime);
643 let output = uc
644 .create_vector(CreateVectorInput {
645 collection: collection.to_string(),
646 dense,
647 content,
648 metadata,
649 link_row: None,
650 link_node: None,
651 })
652 .map_err(|e| format!("{}", e))?;
653
654 let json = created_entity_output_json(&output);
655 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
656 }
657
658 fn tool_insert_document(&self, args: &JsonValue) -> Result<String, String> {
659 let collection = args
660 .get("collection")
661 .and_then(|v| v.as_str())
662 .ok_or("missing required field 'collection'")?;
663 let body = args.get("body").ok_or("missing required field 'body'")?;
664
665 let metadata = parse_metadata_arg(args)?;
666
667 let uc = EntityUseCases::new(&self.runtime);
668 let output = uc
669 .create_document(CreateDocumentInput {
670 collection: collection.to_string(),
671 body: body.clone(),
672 metadata,
673 node_links: vec![],
674 vector_links: vec![],
675 })
676 .map_err(|e| format!("{}", e))?;
677
678 let json = created_entity_output_json(&output);
679 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
680 }
681
682 fn tool_kv_get(&self, args: &JsonValue) -> Result<String, String> {
683 let collection = args
684 .get("collection")
685 .and_then(|v| v.as_str())
686 .ok_or("missing required field 'collection'")?;
687 let key = args
688 .get("key")
689 .and_then(|v| v.as_str())
690 .ok_or("missing required field 'key'")?;
691
692 let uc = EntityUseCases::new(&self.runtime);
693 match uc.get_kv(collection, key).map_err(|e| format!("{}", e))? {
694 Some((value, entity_id)) => {
695 let mut obj = Map::new();
696 obj.insert("found".to_string(), JsonValue::Bool(true));
697 obj.insert("key".to_string(), JsonValue::String(key.to_string()));
698 obj.insert("value".to_string(), storage_value_to_json(&value));
699 obj.insert("rid".to_string(), JsonValue::Number(entity_id.raw() as f64));
700 obj.insert("kind".to_string(), JsonValue::String("kv".to_string()));
701 json_to_string(&JsonValue::Object(obj))
702 .map_err(|e| format!("serialization error: {}", e))
703 }
704 None => {
705 let mut obj = Map::new();
706 obj.insert("found".to_string(), JsonValue::Bool(false));
707 obj.insert("key".to_string(), JsonValue::String(key.to_string()));
708 json_to_string(&JsonValue::Object(obj))
709 .map_err(|e| format!("serialization error: {}", e))
710 }
711 }
712 }
713
714 fn tool_kv_set(&self, args: &JsonValue) -> Result<String, String> {
715 let collection = args
716 .get("collection")
717 .and_then(|v| v.as_str())
718 .ok_or("missing required field 'collection'")?;
719 let key = args
720 .get("key")
721 .and_then(|v| v.as_str())
722 .ok_or("missing required field 'key'")?;
723 let value_arg = args.get("value").ok_or("missing required field 'value'")?;
724
725 let sv = crate::application::entity::json_to_storage_value(value_arg)
726 .map_err(|e| format!("{}", e))?;
727
728 let metadata = parse_metadata_arg(args)?;
729
730 let tags = parse_string_array_arg(args, "tags")?;
731 let ops = crate::runtime::impl_kv::KvAtomicOps::new(&self.runtime);
732 let (_, id) = ops
733 .set_with_tags_and_metadata(collection, key, sv, None, &tags, false, metadata)
734 .map_err(|e| format!("{}", e))?;
735
736 let mut obj = Map::new();
737 obj.insert("ok".to_string(), JsonValue::Bool(true));
738 obj.insert("rid".to_string(), JsonValue::Number(id.raw() as f64));
739 obj.insert("kind".to_string(), JsonValue::String("kv".to_string()));
740 obj.insert(
741 "tags".to_string(),
742 JsonValue::Array(tags.into_iter().map(JsonValue::String).collect()),
743 );
744 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
745 }
746
747 fn tool_kv_invalidate_tags(&self, args: &JsonValue) -> Result<String, String> {
748 let collection = args
749 .get("collection")
750 .and_then(|v| v.as_str())
751 .ok_or("missing required field 'collection'")?;
752 let tags = parse_string_array_arg(args, "tags")?;
753 if tags.is_empty() {
754 return Err("missing required field 'tags'".to_string());
755 }
756 let ops = crate::runtime::impl_kv::KvAtomicOps::new(&self.runtime);
757 let count = ops
758 .invalidate_tags(collection, &tags)
759 .map_err(|e| format!("{}", e))?;
760
761 let mut obj = Map::new();
762 obj.insert("ok".to_string(), JsonValue::Bool(true));
763 obj.insert("invalidated".to_string(), JsonValue::Number(count as f64));
764 obj.insert(
765 "tags".to_string(),
766 JsonValue::Array(tags.into_iter().map(JsonValue::String).collect()),
767 );
768 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
769 }
770
771 fn tool_config_get(&self, args: &JsonValue) -> Result<String, String> {
772 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
773 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
774 self.tool_keyed_query(format!("GET CONFIG {collection} {key}"))
775 }
776
777 fn tool_config_put(&self, args: &JsonValue) -> Result<String, String> {
778 reject_mcp_volatile_options(args, "CONFIG")?;
779 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
780 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
781 let tags = parse_string_array_arg(args, "tags")?;
782 let literal = if let Some(secret_ref) = args.get("secret_ref") {
783 let object = secret_ref
784 .as_object()
785 .ok_or("field 'secret_ref' must be an object")?;
786 let ref_collection = object
787 .get("collection")
788 .and_then(|v| v.as_str())
789 .ok_or_else(|| "secret_ref.collection is required".to_string())
790 .and_then(mcp_keyed_ident)?;
791 let ref_key = object
792 .get("key")
793 .and_then(|v| v.as_str())
794 .ok_or_else(|| "secret_ref.key is required".to_string())
795 .and_then(mcp_keyed_ident)?;
796 format!("SECRET_REF(vault, {ref_collection}.{ref_key})")
797 } else {
798 mcp_value_literal(args.get("value").ok_or("missing required field 'value'")?)?
799 };
800 let mut sql = format!("PUT CONFIG {collection} {key} = {literal}");
801 append_mcp_tags_clause(&mut sql, &tags);
802 self.tool_keyed_query(sql)
803 }
804
805 fn tool_config_resolve(&self, args: &JsonValue) -> Result<String, String> {
806 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
807 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
808 self.tool_keyed_query(format!("RESOLVE CONFIG {collection} {key}"))
809 }
810
811 fn tool_vault_get(&self, args: &JsonValue) -> Result<String, String> {
812 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
813 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
814 self.tool_keyed_query(format!("VAULT GET {collection}.{key}"))
815 }
816
817 fn tool_vault_put(&self, args: &JsonValue) -> Result<String, String> {
818 reject_mcp_volatile_options(args, "VAULT")?;
819 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
820 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
821 let value = mcp_value_literal(args.get("value").ok_or("missing required field 'value'")?)?;
822 let tags = parse_string_array_arg(args, "tags")?;
823 let mut sql = format!("VAULT PUT {collection}.{key} = {value}");
824 append_mcp_tags_clause(&mut sql, &tags);
825 self.tool_keyed_query(sql)
826 }
827
828 fn tool_vault_unseal(&self, args: &JsonValue) -> Result<String, String> {
829 let collection = mcp_keyed_ident(get_str_field(args, "collection")?)?;
830 let key = mcp_keyed_ident(get_str_field(args, "key")?)?;
831 self.tool_keyed_query(format!("VAULT REVEAL {collection}.{key}"))
832 }
833
834 fn tool_keyed_query(&self, sql: String) -> Result<String, String> {
835 let uc = &self.runtime;
836 let result = uc.execute_query(&(sql)).map_err(|e| format!("{}", e))?;
837 let json = runtime_query_json(&result, &None, &None);
838 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
839 }
840
841 fn tool_delete(&self, args: &JsonValue) -> Result<String, String> {
842 let collection = args
843 .get("collection")
844 .and_then(|v| v.as_str())
845 .ok_or("missing required field 'collection'")?;
846 let id = args
847 .get("id")
848 .and_then(|v| v.as_u64())
849 .ok_or("missing required field 'id' (integer)")?;
850
851 let uc = EntityUseCases::new(&self.runtime);
852 let output = uc
853 .delete(DeleteEntityInput {
854 collection: collection.to_string(),
855 id: EntityId::new(id),
856 })
857 .map_err(|e| format!("{}", e))?;
858
859 let mut obj = Map::new();
860 obj.insert("deleted".to_string(), JsonValue::Bool(output.deleted));
861 obj.insert("id".to_string(), JsonValue::Number(output.id.raw() as f64));
862 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
863 }
864
865 fn tool_search_vector(&self, args: &JsonValue) -> Result<String, String> {
866 let collection = args
867 .get("collection")
868 .and_then(|v| v.as_str())
869 .ok_or("missing required field 'collection'")?;
870 let vector_arr = args
871 .get("vector")
872 .and_then(|v| v.as_array())
873 .ok_or("missing required field 'vector' (array of numbers)")?;
874
875 let mut vector = Vec::with_capacity(vector_arr.len());
876 for v in vector_arr {
877 vector.push(
878 v.as_f64()
879 .ok_or("'vector' array must contain only numbers")? as f32,
880 );
881 }
882 let k = args
883 .get("k")
884 .and_then(|v| v.as_u64())
885 .map(|v| v as usize)
886 .unwrap_or(10);
887 let min_score = args
888 .get("min_score")
889 .and_then(|v| v.as_f64())
890 .map(|v| v as f32)
891 .unwrap_or(0.0);
892
893 let uc = &self.runtime;
894 let results = uc
895 .search_similar_input(SearchSimilarInput {
896 collection: collection.to_string(),
897 vector,
898 k,
899 min_score,
900 text: None,
901 provider: None,
902 })
903 .map_err(|e| format!("{}", e))?;
904
905 let items: Vec<JsonValue> = results
906 .iter()
907 .map(|r| {
908 let mut obj = Map::new();
909 obj.insert(
910 "rid".to_string(),
911 JsonValue::Number(r.entity_id.raw() as f64),
912 );
913 obj.insert("kind".to_string(), JsonValue::String("vector".to_string()));
914 obj.insert("score".to_string(), JsonValue::Number(r.score as f64));
915 obj.insert("distance".to_string(), JsonValue::Number(r.distance as f64));
916 JsonValue::Object(obj)
917 })
918 .collect();
919
920 let mut obj = Map::new();
921 obj.insert("count".to_string(), JsonValue::Number(items.len() as f64));
922 obj.insert("results".to_string(), JsonValue::Array(items));
923 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
924 }
925
926 fn tool_search_text(&self, args: &JsonValue) -> Result<String, String> {
927 let query = args
928 .get("query")
929 .and_then(|v| v.as_str())
930 .ok_or("missing required field 'query'")?;
931
932 let collections = args
933 .get("collections")
934 .and_then(|v| v.as_array())
935 .map(|arr| {
936 arr.iter()
937 .filter_map(|v| v.as_str().map(String::from))
938 .collect::<Vec<_>>()
939 });
940 let limit = args
941 .get("limit")
942 .and_then(|v| v.as_u64())
943 .map(|v| v as usize);
944 let fuzzy = args.get("fuzzy").and_then(|v| v.as_bool()).unwrap_or(false);
945
946 let uc = &self.runtime;
947 let result = uc
948 .search_text_input(SearchTextInput {
949 query: query.to_string(),
950 collections,
951 entity_types: None,
952 capabilities: None,
953 fields: None,
954 limit,
955 fuzzy,
956 })
957 .map_err(|e| format!("{}", e))?;
958
959 let items: Vec<JsonValue> = result
960 .matches
961 .iter()
962 .map(|m| {
963 let mut obj = Map::new();
964 obj.insert(
965 "rid".to_string(),
966 JsonValue::Number(m.entity.id.raw() as f64),
967 );
968 obj.insert(
969 "kind".to_string(),
970 JsonValue::String(m.entity.kind.storage_type().to_string()),
971 );
972 obj.insert("score".to_string(), JsonValue::Number(m.score as f64));
973 JsonValue::Object(obj)
974 })
975 .collect();
976
977 let mut obj = Map::new();
978 obj.insert("count".to_string(), JsonValue::Number(items.len() as f64));
979 obj.insert("results".to_string(), JsonValue::Array(items));
980 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
981 }
982
983 fn tool_health(&self) -> Result<String, String> {
984 let uc = &self.runtime;
985 let stats = uc.stats();
986 let json = runtime_stats_json(&stats);
987 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
988 }
989
990 fn tool_graph_traverse(&self, args: &JsonValue) -> Result<String, String> {
991 let source = args
992 .get("source")
993 .and_then(|v| v.as_str())
994 .ok_or("missing required field 'source'")?;
995 let direction = parse_direction(args.get("direction").and_then(|v| v.as_str()));
996 let max_depth = args
997 .get("max_depth")
998 .and_then(|v| v.as_u64())
999 .map(|v| v as usize)
1000 .unwrap_or(3);
1001 let strategy = match args.get("strategy").and_then(|v| v.as_str()) {
1002 Some("dfs") => RuntimeGraphTraversalStrategy::Dfs,
1003 _ => RuntimeGraphTraversalStrategy::Bfs,
1004 };
1005
1006 let uc = GraphUseCases::new(&self.runtime);
1007 let result = uc
1008 .traverse(GraphTraversalInput {
1009 source: source.to_string(),
1010 direction,
1011 max_depth,
1012 strategy,
1013 edge_labels: None,
1014 projection: None,
1015 })
1016 .map_err(|e| format!("{}", e))?;
1017
1018 let visits: Vec<JsonValue> = result
1019 .visits
1020 .iter()
1021 .map(|v| {
1022 let mut obj = Map::new();
1023 obj.insert("depth".to_string(), JsonValue::Number(v.depth as f64));
1024 obj.insert("node_id".to_string(), JsonValue::String(v.node.id.clone()));
1025 obj.insert("label".to_string(), JsonValue::String(v.node.label.clone()));
1026 obj.insert(
1027 "node_type".to_string(),
1028 JsonValue::String(v.node.node_type.clone()),
1029 );
1030 JsonValue::Object(obj)
1031 })
1032 .collect();
1033
1034 let edges: Vec<JsonValue> = result
1035 .edges
1036 .iter()
1037 .map(|e| {
1038 let mut obj = Map::new();
1039 obj.insert("source".to_string(), JsonValue::String(e.source.clone()));
1040 obj.insert("target".to_string(), JsonValue::String(e.target.clone()));
1041 obj.insert(
1042 "edge_type".to_string(),
1043 JsonValue::String(e.edge_type.clone()),
1044 );
1045 obj.insert("weight".to_string(), JsonValue::Number(e.weight as f64));
1046 JsonValue::Object(obj)
1047 })
1048 .collect();
1049
1050 let mut obj = Map::new();
1051 obj.insert(
1052 "source".to_string(),
1053 JsonValue::String(result.source.clone()),
1054 );
1055 obj.insert(
1056 "visit_count".to_string(),
1057 JsonValue::Number(visits.len() as f64),
1058 );
1059 obj.insert("visits".to_string(), JsonValue::Array(visits));
1060 obj.insert("edges".to_string(), JsonValue::Array(edges));
1061 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
1062 }
1063
1064 fn tool_graph_shortest_path(&self, args: &JsonValue) -> Result<String, String> {
1065 let source = args
1066 .get("source")
1067 .and_then(|v| v.as_str())
1068 .ok_or("missing required field 'source'")?;
1069 let target = args
1070 .get("target")
1071 .and_then(|v| v.as_str())
1072 .ok_or("missing required field 'target'")?;
1073 let direction = parse_direction(args.get("direction").and_then(|v| v.as_str()));
1074 let algorithm = match args.get("algorithm").and_then(|v| v.as_str()) {
1075 Some("astar") | Some("a*") => RuntimeGraphPathAlgorithm::AStar,
1076 Some("bellman_ford") | Some("bellmanford") => RuntimeGraphPathAlgorithm::BellmanFord,
1077 Some("dijkstra") => RuntimeGraphPathAlgorithm::Dijkstra,
1078 _ => RuntimeGraphPathAlgorithm::Bfs,
1079 };
1080
1081 let uc = GraphUseCases::new(&self.runtime);
1082 let result = uc
1083 .shortest_path(GraphShortestPathInput {
1084 source: source.to_string(),
1085 target: target.to_string(),
1086 direction,
1087 algorithm,
1088 edge_labels: None,
1089 projection: None,
1090 })
1091 .map_err(|e| format!("{}", e))?;
1092
1093 let mut obj = Map::new();
1094 obj.insert(
1095 "source".to_string(),
1096 JsonValue::String(result.source.clone()),
1097 );
1098 obj.insert(
1099 "target".to_string(),
1100 JsonValue::String(result.target.clone()),
1101 );
1102 obj.insert(
1103 "nodes_visited".to_string(),
1104 JsonValue::Number(result.nodes_visited as f64),
1105 );
1106
1107 match &result.path {
1108 Some(path) => {
1109 obj.insert("found".to_string(), JsonValue::Bool(true));
1110 obj.insert(
1111 "hop_count".to_string(),
1112 JsonValue::Number(path.hop_count as f64),
1113 );
1114 obj.insert(
1115 "total_weight".to_string(),
1116 JsonValue::Number(path.total_weight),
1117 );
1118 let nodes_json: Vec<JsonValue> = path
1119 .nodes
1120 .iter()
1121 .map(|n| {
1122 let mut nobj = Map::new();
1123 nobj.insert("id".to_string(), JsonValue::String(n.id.clone()));
1124 nobj.insert("label".to_string(), JsonValue::String(n.label.clone()));
1125 JsonValue::Object(nobj)
1126 })
1127 .collect();
1128 obj.insert("nodes".to_string(), JsonValue::Array(nodes_json));
1129 }
1130 None => {
1131 obj.insert("found".to_string(), JsonValue::Bool(false));
1132 }
1133 }
1134
1135 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
1136 }
1137
1138 fn tool_update(&self, args: &JsonValue) -> Result<String, String> {
1139 let collection = get_str_field(args, "collection")?;
1140 let set_obj = args.get("set").ok_or("missing 'set'")?;
1141 let where_clause = args
1142 .get("where_filter")
1143 .and_then(|v| v.as_str())
1144 .unwrap_or("");
1145
1146 use crate::server::handlers_query::is_safe_sql_identifier;
1151 if !is_safe_sql_identifier(collection) {
1152 return Err(format!(
1153 "invalid collection name '{collection}': expected [A-Za-z_][A-Za-z0-9_]*"
1154 ));
1155 }
1156 let mut sql = format!("UPDATE {} SET ", collection);
1157 if let Some(obj) = set_obj.as_object() {
1158 let assignments: Vec<String> = obj
1159 .iter()
1160 .map(|(k, v)| {
1161 if !is_safe_sql_identifier(k) {
1162 return Err(format!(
1163 "invalid column name '{k}': expected [A-Za-z_][A-Za-z0-9_]*"
1164 ));
1165 }
1166 Ok(format!(
1167 "{} = {}",
1168 k,
1169 crate::rpc_stdio::value_to_sql_literal(v)
1170 ))
1171 })
1172 .collect::<Result<_, String>>()?;
1173 sql.push_str(&assignments.join(", "));
1174 } else {
1175 return Err("'set' must be a JSON object".to_string());
1176 }
1177 if !where_clause.is_empty() {
1178 sql.push_str(&format!(" WHERE {}", where_clause));
1179 }
1180
1181 let uc = &self.runtime;
1182 let result = uc.execute_query(&(sql)).map_err(|e| format!("{}", e))?;
1183
1184 let mut resp = Map::new();
1185 resp.insert("ok".into(), JsonValue::Bool(true));
1186 resp.insert(
1187 "affected_rows".into(),
1188 JsonValue::Number(result.affected_rows as f64),
1189 );
1190 json_to_string(&JsonValue::Object(resp)).map_err(|e| format!("serialization error: {}", e))
1191 }
1192
1193 fn tool_scan(&self, args: &JsonValue) -> Result<String, String> {
1194 let collection = get_str_field(args, "collection")?;
1195 let limit = args
1196 .get("limit")
1197 .and_then(|v| v.as_u64())
1198 .map(|v| v as usize)
1199 .unwrap_or(10);
1200 let offset = args
1201 .get("offset")
1202 .and_then(|v| v.as_u64())
1203 .map(|v| v as usize)
1204 .unwrap_or(0);
1205
1206 let uc = &self.runtime;
1207 let page = uc
1208 .scan(ScanCollectionInput {
1209 collection: collection.to_string(),
1210 offset,
1211 limit,
1212 })
1213 .map_err(|e| format!("{}", e))?;
1214
1215 let json = crate::presentation::entity_json::scan_page_json(&page);
1216 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1217 }
1218
1219 fn tool_graph_centrality(&self, args: &JsonValue) -> Result<String, String> {
1220 let algorithm_str = get_str_field(args, "algorithm")?;
1221 let algo = match algorithm_str {
1222 "degree" => RuntimeGraphCentralityAlgorithm::Degree,
1223 "closeness" => RuntimeGraphCentralityAlgorithm::Closeness,
1224 "betweenness" => RuntimeGraphCentralityAlgorithm::Betweenness,
1225 "eigenvector" => RuntimeGraphCentralityAlgorithm::Eigenvector,
1226 "pagerank" => RuntimeGraphCentralityAlgorithm::PageRank,
1227 _ => return Err(format!("unknown algorithm: {algorithm_str}")),
1228 };
1229
1230 let uc = GraphUseCases::new(&self.runtime);
1231 let result = uc
1232 .centrality(GraphCentralityInput {
1233 algorithm: algo,
1234 top_k: 100,
1235 normalize: true,
1236 max_iterations: None,
1237 epsilon: None,
1238 alpha: None,
1239 projection: None,
1240 })
1241 .map_err(|e| format!("{}", e))?;
1242
1243 let json = crate::presentation::graph_json::graph_centrality_json(&result);
1244 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1245 }
1246
1247 fn tool_graph_community(&self, args: &JsonValue) -> Result<String, String> {
1248 let algorithm_str = get_str_field(args, "algorithm")?;
1249 let algo = match algorithm_str {
1250 "label_propagation" => RuntimeGraphCommunityAlgorithm::LabelPropagation,
1251 "louvain" => RuntimeGraphCommunityAlgorithm::Louvain,
1252 _ => return Err(format!("unknown algorithm: {algorithm_str}")),
1253 };
1254 let max_iterations = args
1255 .get("max_iterations")
1256 .and_then(|v| v.as_u64())
1257 .map(|v| v as usize);
1258
1259 let uc = GraphUseCases::new(&self.runtime);
1260 let result = uc
1261 .communities(GraphCommunitiesInput {
1262 algorithm: algo,
1263 min_size: 1,
1264 max_iterations,
1265 resolution: None,
1266 projection: None,
1267 })
1268 .map_err(|e| format!("{}", e))?;
1269
1270 let json = crate::presentation::graph_json::graph_community_json(&result);
1271 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1272 }
1273
1274 fn tool_graph_components(&self, args: &JsonValue) -> Result<String, String> {
1275 let mode = match args.get("mode").and_then(|v| v.as_str()) {
1276 Some("strongly_connected") => RuntimeGraphComponentsMode::Strong,
1277 _ => RuntimeGraphComponentsMode::Weak,
1278 };
1279
1280 let uc = GraphUseCases::new(&self.runtime);
1281 let result = uc
1282 .components(GraphComponentsInput {
1283 mode,
1284 min_size: 1,
1285 projection: None,
1286 })
1287 .map_err(|e| format!("{}", e))?;
1288
1289 let json = crate::presentation::graph_json::graph_components_json(&result);
1290 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1291 }
1292
1293 fn tool_graph_cycles(&self, args: &JsonValue) -> Result<String, String> {
1294 let max_length = args
1295 .get("max_length")
1296 .and_then(|v| v.as_u64())
1297 .map(|v| v as usize)
1298 .unwrap_or(10);
1299 let max_cycles = args
1300 .get("max_cycles")
1301 .and_then(|v| v.as_u64())
1302 .map(|v| v as usize)
1303 .unwrap_or(100);
1304
1305 let uc = GraphUseCases::new(&self.runtime);
1306 let result = uc
1307 .cycles(GraphCyclesInput {
1308 max_length,
1309 max_cycles,
1310 projection: None,
1311 })
1312 .map_err(|e| format!("{}", e))?;
1313
1314 let json = crate::presentation::graph_json::graph_cycles_json(&result);
1315 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1316 }
1317
1318 fn tool_graph_clustering(&self, _args: &JsonValue) -> Result<String, String> {
1319 let uc = GraphUseCases::new(&self.runtime);
1320 let result = uc
1321 .clustering(GraphClusteringInput {
1322 top_k: 100,
1323 include_triangles: true,
1324 projection: None,
1325 })
1326 .map_err(|e| format!("{}", e))?;
1327
1328 let json = crate::presentation::graph_json::graph_clustering_json(&result);
1329 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1330 }
1331
1332 fn tool_create_collection(&self, args: &JsonValue) -> Result<String, String> {
1333 let name = get_str_field(args, "name")?;
1334 self.runtime
1335 .db()
1336 .store()
1337 .create_collection(name)
1338 .map_err(|e| format!("{e:?}"))?;
1339 let mut resp = Map::new();
1340 resp.insert("ok".into(), JsonValue::Bool(true));
1341 resp.insert("collection".into(), JsonValue::String(name.to_string()));
1342 json_to_string(&JsonValue::Object(resp)).map_err(|e| format!("serialization error: {}", e))
1343 }
1344
1345 fn tool_drop_collection(&self, args: &JsonValue) -> Result<String, String> {
1346 let name = get_str_field(args, "name")?;
1347 self.runtime
1348 .db()
1349 .store()
1350 .drop_collection(name)
1351 .map_err(|e| format!("{e:?}"))?;
1352 let mut resp = Map::new();
1353 resp.insert("ok".into(), JsonValue::Bool(true));
1354 resp.insert("dropped".into(), JsonValue::String(name.to_string()));
1355 json_to_string(&JsonValue::Object(resp)).map_err(|e| format!("serialization error: {}", e))
1356 }
1357
1358 fn tool_rql_validate(&self, args: &JsonValue) -> Result<String, String> {
1359 let rql = get_str_field(args, "rql")?;
1360 let verdict = reddb_rql::knowledge::validate_rql(rql);
1361 let json = rql_validation_json(&verdict);
1362 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1363 }
1364
1365 fn tool_rql_explain(&self, args: &JsonValue) -> Result<String, String> {
1366 let rql = get_str_field(args, "rql")?;
1367 let explanation = reddb_rql::knowledge::explain_rql(rql);
1368
1369 let mut obj = Map::new();
1370 obj.insert(
1371 "validation".to_string(),
1372 rql_validation_json(&explanation.validation),
1373 );
1374 if let Some(ast) = &explanation.ast {
1375 obj.insert("ast".to_string(), JsonValue::String(ast.clone()));
1376 }
1377 if let Some(optimized) = &explanation.optimized_ast {
1378 obj.insert(
1379 "optimized_ast".to_string(),
1380 JsonValue::String(optimized.clone()),
1381 );
1382 }
1383 obj.insert(
1384 "applied_passes".to_string(),
1385 JsonValue::Array(
1386 explanation
1387 .applied_passes
1388 .iter()
1389 .cloned()
1390 .map(JsonValue::String)
1391 .collect(),
1392 ),
1393 );
1394 json_to_string(&JsonValue::Object(obj)).map_err(|e| format!("serialization error: {}", e))
1395 }
1396
1397 fn tool_type_of(&self, args: &JsonValue) -> Result<String, String> {
1403 let type_name = args.get("type").and_then(|v| v.as_str());
1404 let value = args.get("value");
1405 if type_name.is_none() && value.is_none() {
1406 return Err(
1407 "provide one of 'type' (a type name) or 'value' (a JSON literal)".to_string(),
1408 );
1409 }
1410 let facts =
1411 reddb_types::knowledge::type_of_json(type_name, value).ok_or_else(
1412 || match type_name {
1413 Some(name) => format!("unknown type name '{name}'"),
1414 None => "could not resolve a type from the request".to_string(),
1415 },
1416 )?;
1417 json_to_string(&facts).map_err(|e| format!("serialization error: {}", e))
1418 }
1419
1420 fn tool_explain_connection(&self, args: &JsonValue) -> Result<String, String> {
1421 let uri = get_str_field(args, "uri")?;
1422 let target = reddb_wire::parse(uri).map_err(|err| err.to_string())?;
1423 let json = connection_explanation_json(uri, &target);
1424 json_to_string(&json).map_err(|e| format!("serialization error: {}", e))
1425 }
1426}
1427
1428fn format_mcp_ask_parse_error(err: crate::runtime::ai::mcp_ask_tool::ParseError) -> String {
1433 use crate::runtime::ai::mcp_ask_tool::ParseError;
1434
1435 match err {
1436 ParseError::NotAnObject => "arguments must be an object".to_string(),
1437 ParseError::MissingQuestion => "missing required field 'question'".to_string(),
1438 ParseError::QuestionWrongType => "field 'question' must be a string".to_string(),
1439 ParseError::WrongType { path, expected } => {
1440 format!("{path} must be {expected}")
1441 }
1442 ParseError::OutOfRange { path, detail } => {
1443 format!("{path} out of range: {detail}")
1444 }
1445 ParseError::CacheAndNocache => {
1446 "options.cache and options.nocache are mutually exclusive".to_string()
1447 }
1448 ParseError::UnknownOption { path } => format!("unknown option {path}"),
1449 }
1450}
1451
1452fn connection_explanation_json(uri: &str, target: &reddb_wire::ConnectionTarget) -> JsonValue {
1453 let scheme = connection_uri_scheme(uri);
1454 let mut obj = Map::new();
1455 obj.insert("uri".to_string(), JsonValue::String(uri.to_string()));
1456 obj.insert("scheme".to_string(), JsonValue::String(scheme.clone()));
1457
1458 let mut target_obj = Map::new();
1459 match target {
1460 reddb_wire::ConnectionTarget::Memory => {
1461 obj.insert(
1462 "mode".to_string(),
1463 JsonValue::String("embedded".to_string()),
1464 );
1465 obj.insert(
1466 "transport".to_string(),
1467 JsonValue::String("memory".to_string()),
1468 );
1469 obj.insert(
1470 "topology".to_string(),
1471 JsonValue::String("ephemeral".to_string()),
1472 );
1473 target_obj.insert("kind".to_string(), JsonValue::String("memory".to_string()));
1474 }
1475 reddb_wire::ConnectionTarget::File { path } => {
1476 obj.insert(
1477 "mode".to_string(),
1478 JsonValue::String("embedded".to_string()),
1479 );
1480 obj.insert(
1481 "transport".to_string(),
1482 JsonValue::String("filesystem".to_string()),
1483 );
1484 obj.insert(
1485 "topology".to_string(),
1486 JsonValue::String("persisted".to_string()),
1487 );
1488 target_obj.insert("kind".to_string(), JsonValue::String("file".to_string()));
1489 target_obj.insert(
1490 "path".to_string(),
1491 JsonValue::String(path.display().to_string()),
1492 );
1493 }
1494 reddb_wire::ConnectionTarget::Grpc { endpoint } => {
1495 obj.insert("mode".to_string(), JsonValue::String("remote".to_string()));
1496 obj.insert(
1497 "transport".to_string(),
1498 JsonValue::String(
1499 if scheme == "grpcs" {
1500 "grpc_tls"
1501 } else {
1502 "grpc"
1503 }
1504 .to_string(),
1505 ),
1506 );
1507 obj.insert(
1508 "topology".to_string(),
1509 JsonValue::String("single".to_string()),
1510 );
1511 target_obj.insert("kind".to_string(), JsonValue::String("grpc".to_string()));
1512 target_obj.insert("endpoint".to_string(), JsonValue::String(endpoint.clone()));
1513 }
1514 reddb_wire::ConnectionTarget::GrpcCluster {
1515 primary,
1516 replicas,
1517 force_primary,
1518 } => {
1519 obj.insert("mode".to_string(), JsonValue::String("remote".to_string()));
1520 obj.insert(
1521 "transport".to_string(),
1522 JsonValue::String(cluster_transport_label(&scheme).to_string()),
1523 );
1524 obj.insert(
1525 "topology".to_string(),
1526 JsonValue::String("primary_replica".to_string()),
1527 );
1528 target_obj.insert(
1529 "kind".to_string(),
1530 JsonValue::String("primary_replica".to_string()),
1531 );
1532 target_obj.insert("primary".to_string(), JsonValue::String(primary.clone()));
1533 target_obj.insert(
1534 "replicas".to_string(),
1535 JsonValue::Array(replicas.iter().cloned().map(JsonValue::String).collect()),
1536 );
1537 target_obj.insert("force_primary".to_string(), JsonValue::Bool(*force_primary));
1538 }
1539 reddb_wire::ConnectionTarget::Http { base_url } => {
1540 obj.insert("mode".to_string(), JsonValue::String("remote".to_string()));
1541 obj.insert(
1542 "transport".to_string(),
1543 JsonValue::String(if scheme == "https" { "https" } else { "http" }.to_string()),
1544 );
1545 obj.insert(
1546 "topology".to_string(),
1547 JsonValue::String("single".to_string()),
1548 );
1549 target_obj.insert("kind".to_string(), JsonValue::String("http".to_string()));
1550 target_obj.insert("base_url".to_string(), JsonValue::String(base_url.clone()));
1551 }
1552 reddb_wire::ConnectionTarget::RedWire { host, port, tls } => {
1553 obj.insert("mode".to_string(), JsonValue::String("remote".to_string()));
1554 obj.insert(
1555 "transport".to_string(),
1556 JsonValue::String(if *tls { "redwire_tls" } else { "redwire" }.to_string()),
1557 );
1558 obj.insert(
1559 "topology".to_string(),
1560 JsonValue::String("single".to_string()),
1561 );
1562 target_obj.insert("kind".to_string(), JsonValue::String("redwire".to_string()));
1563 target_obj.insert("host".to_string(), JsonValue::String(host.clone()));
1564 target_obj.insert("port".to_string(), JsonValue::Number(*port as f64));
1565 target_obj.insert("tls".to_string(), JsonValue::Bool(*tls));
1566 }
1567 reddb_wire::ConnectionTarget::WsNative { host, port, tls } => {
1568 obj.insert("mode".to_string(), JsonValue::String("remote".to_string()));
1569 obj.insert(
1570 "transport".to_string(),
1571 JsonValue::String(if *tls { "redwire_wss" } else { "redwire_ws" }.to_string()),
1572 );
1573 obj.insert(
1574 "topology".to_string(),
1575 JsonValue::String("single".to_string()),
1576 );
1577 target_obj.insert(
1578 "kind".to_string(),
1579 JsonValue::String("redwire_websocket".to_string()),
1580 );
1581 target_obj.insert("host".to_string(), JsonValue::String(host.clone()));
1582 target_obj.insert("port".to_string(), JsonValue::Number(*port as f64));
1583 target_obj.insert("tls".to_string(), JsonValue::Bool(*tls));
1584 }
1585 }
1586 obj.insert("target".to_string(), JsonValue::Object(target_obj));
1587 JsonValue::Object(obj)
1588}
1589
1590fn connection_uri_scheme(uri: &str) -> String {
1591 uri.split_once(':')
1592 .map(|(scheme, _)| scheme.to_ascii_lowercase())
1593 .unwrap_or_default()
1594}
1595
1596fn cluster_transport_label(scheme: &str) -> &'static str {
1597 match scheme {
1598 "red" => "redwire",
1599 "reds" => "redwire_tls",
1600 "grpcs" => "grpc_tls",
1601 _ => "grpc",
1602 }
1603}
1604
1605fn rql_validation_json(verdict: &reddb_rql::knowledge::RqlValidation) -> JsonValue {
1608 let mut obj = Map::new();
1609 obj.insert("valid".to_string(), JsonValue::Bool(verdict.valid));
1610 if let Some(statement) = &verdict.statement {
1611 obj.insert(
1612 "statement".to_string(),
1613 JsonValue::String(statement.clone()),
1614 );
1615 }
1616 obj.insert(
1617 "has_with_clause".to_string(),
1618 JsonValue::Bool(verdict.has_with_clause),
1619 );
1620 if let Some(error) = &verdict.error {
1621 obj.insert("error".to_string(), rql_diagnostic_json(error));
1622 }
1623 JsonValue::Object(obj)
1624}
1625
1626fn rql_diagnostic_json(diag: &reddb_rql::knowledge::RqlDiagnostic) -> JsonValue {
1628 let mut obj = Map::new();
1629 obj.insert(
1630 "message".to_string(),
1631 JsonValue::String(diag.message.clone()),
1632 );
1633 obj.insert("line".to_string(), JsonValue::Number(diag.line as f64));
1634 obj.insert("column".to_string(), JsonValue::Number(diag.column as f64));
1635 obj.insert("offset".to_string(), JsonValue::Number(diag.offset as f64));
1636 obj.insert("kind".to_string(), JsonValue::String(diag.kind.clone()));
1637 obj.insert(
1638 "expected".to_string(),
1639 JsonValue::Array(
1640 diag.expected
1641 .iter()
1642 .cloned()
1643 .map(JsonValue::String)
1644 .collect(),
1645 ),
1646 );
1647 JsonValue::Object(obj)
1648}
1649
1650fn parse_direction(s: Option<&str>) -> RuntimeGraphDirection {
1651 match s {
1652 Some("incoming") => RuntimeGraphDirection::Incoming,
1653 Some("both") => RuntimeGraphDirection::Both,
1654 _ => RuntimeGraphDirection::Outgoing,
1655 }
1656}
1657
1658fn parse_metadata_arg(
1660 args: &JsonValue,
1661) -> Result<Vec<(String, crate::storage::unified::MetadataValue)>, String> {
1662 match args.get("metadata").and_then(|v| v.as_object()) {
1663 Some(obj) => {
1664 let mut out = Vec::with_capacity(obj.len());
1665 for (key, value) in obj {
1666 let mv = crate::application::entity::json_to_metadata_value(value)
1667 .map_err(|e| format!("{}", e))?;
1668 out.push((key.clone(), mv));
1669 }
1670 Ok(out)
1671 }
1672 None => Ok(vec![]),
1673 }
1674}
1675
1676fn parse_string_array_arg(args: &JsonValue, field: &str) -> Result<Vec<String>, String> {
1677 match args.get(field) {
1678 None | Some(JsonValue::Null) => Ok(Vec::new()),
1679 Some(JsonValue::Array(values)) => values
1680 .iter()
1681 .map(|value| {
1682 value
1683 .as_str()
1684 .map(ToOwned::to_owned)
1685 .ok_or_else(|| format!("field '{field}' must be an array of strings"))
1686 })
1687 .collect(),
1688 _ => Err(format!("field '{field}' must be an array of strings")),
1689 }
1690}
1691
1692fn mcp_keyed_ident(value: &str) -> Result<String, String> {
1693 if !value.is_empty()
1694 && value
1695 .bytes()
1696 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.')
1697 {
1698 Ok(value.to_string())
1699 } else {
1700 Err(
1701 "keyed collection and key names must use letters, numbers, underscores, or dots"
1702 .to_string(),
1703 )
1704 }
1705}
1706
1707fn mcp_value_literal(value: &JsonValue) -> Result<String, String> {
1708 match value {
1709 JsonValue::String(value) => Ok(format!("'{}'", value.replace('\'', "''"))),
1710 JsonValue::Integer(value) => Ok(value.to_string()),
1711 JsonValue::Number(value) => Ok(value.to_string()),
1712 JsonValue::Decimal(value) => Ok(value.clone()),
1713 JsonValue::Bool(value) => Ok(value.to_string()),
1714 JsonValue::Null => Ok("NULL".to_string()),
1715 JsonValue::Array(_) | JsonValue::Object(_) => {
1716 json_to_string(value).map_err(|err| format!("serialization error: {err}"))
1717 }
1718 }
1719}
1720
1721fn append_mcp_tags_clause(sql: &mut String, tags: &[String]) {
1722 if tags.is_empty() {
1723 return;
1724 }
1725 sql.push_str(" TAGS [");
1726 for (index, tag) in tags.iter().enumerate() {
1727 if index > 0 {
1728 sql.push_str(", ");
1729 }
1730 sql.push('\'');
1731 sql.push_str(&tag.replace('\'', "''"));
1732 sql.push('\'');
1733 }
1734 sql.push(']');
1735}
1736
1737fn reject_mcp_volatile_options(args: &JsonValue, domain: &str) -> Result<(), String> {
1738 for field in ["ttl", "ttl_ms", "expire", "expire_ms", "expires_at"] {
1739 if args.get(field).is_some() {
1740 return Err(format!(
1741 "{domain} does not support TTL or expiration options"
1742 ));
1743 }
1744 }
1745 Ok(())
1746}
1747
1748fn get_str_field<'a>(args: &'a JsonValue, field: &str) -> Result<&'a str, String> {
1750 args.get(field)
1751 .and_then(|v| v.as_str())
1752 .ok_or_else(|| format!("missing '{field}'"))
1753}
1754
1755impl McpServer {
1757 fn tool_auth_bootstrap(&self, args: &JsonValue) -> Result<String, String> {
1758 let username = get_str_field(args, "username")?;
1759 let password = get_str_field(args, "password")?;
1760
1761 let br = self
1762 .auth_store
1763 .bootstrap(username, password)
1764 .map_err(|e| e.to_string())?;
1765
1766 let mut result = Map::new();
1767 result.insert("ok".into(), JsonValue::Bool(true));
1768 result.insert("username".into(), JsonValue::String(br.user.username));
1769 result.insert(
1770 "role".into(),
1771 JsonValue::String(br.user.role.as_str().into()),
1772 );
1773 result.insert("api_key".into(), JsonValue::String(br.api_key.key));
1774 result.insert("api_key_name".into(), JsonValue::String(br.api_key.name));
1775 if let Some(cert) = br.certificate {
1776 result.insert("certificate".into(), JsonValue::String(cert));
1777 result.insert(
1778 "message".into(),
1779 JsonValue::String(
1780 "Save this certificate — it is the ONLY way to unseal the vault after restart."
1781 .into(),
1782 ),
1783 );
1784 } else {
1785 result.insert(
1786 "message".into(),
1787 JsonValue::String(
1788 "First admin user created. Save the API key — it won't be shown again.".into(),
1789 ),
1790 );
1791 }
1792 json_to_string(&JsonValue::Object(result))
1793 }
1794
1795 fn tool_auth_create_user(&self, args: &JsonValue) -> Result<String, String> {
1796 let username = get_str_field(args, "username")?;
1797 let password = get_str_field(args, "password")?;
1798 let role_str = get_str_field(args, "role")?;
1799 let role = Role::from_str(role_str).ok_or_else(|| format!("invalid role: {role_str}"))?;
1800
1801 self.auth_store
1802 .create_user(username, password, role)
1803 .map_err(|e| e.to_string())?;
1804
1805 let mut result = Map::new();
1806 result.insert("ok".into(), JsonValue::Bool(true));
1807 result.insert("username".into(), JsonValue::String(username.into()));
1808 result.insert("role".into(), JsonValue::String(role.as_str().into()));
1809 json_to_string(&JsonValue::Object(result))
1810 }
1811
1812 fn tool_auth_login(&self, args: &JsonValue) -> Result<String, String> {
1813 let username = get_str_field(args, "username")?;
1814 let password = get_str_field(args, "password")?;
1815
1816 let session = self
1817 .auth_store
1818 .authenticate_active(None, username, password)
1819 .map_err(|e| e.to_string())?;
1820
1821 let mut result = Map::new();
1822 result.insert("ok".into(), JsonValue::Bool(true));
1823 result.insert("token".into(), JsonValue::String(session.token));
1824 result.insert("username".into(), JsonValue::String(session.username));
1825 result.insert(
1826 "role".into(),
1827 JsonValue::String(session.role.as_str().into()),
1828 );
1829 result.insert(
1830 "expires_at".into(),
1831 JsonValue::Number(session.expires_at as f64),
1832 );
1833 json_to_string(&JsonValue::Object(result))
1834 }
1835
1836 fn tool_auth_create_api_key(&self, args: &JsonValue) -> Result<String, String> {
1837 let username = get_str_field(args, "username")?;
1838 let name = get_str_field(args, "name")?;
1839 let role_str = get_str_field(args, "role")?;
1840 let role = Role::from_str(role_str).ok_or_else(|| format!("invalid role: {role_str}"))?;
1841
1842 let key = self
1843 .auth_store
1844 .create_api_key(username, name, role)
1845 .map_err(|e| e.to_string())?;
1846
1847 let mut result = Map::new();
1848 result.insert("ok".into(), JsonValue::Bool(true));
1849 result.insert("key".into(), JsonValue::String(key.key));
1850 result.insert("name".into(), JsonValue::String(key.name));
1851 result.insert("role".into(), JsonValue::String(key.role.as_str().into()));
1852 json_to_string(&JsonValue::Object(result))
1853 }
1854
1855 fn tool_auth_list_users(&self) -> Result<String, String> {
1856 let users = self.auth_store.list_users();
1857 let arr: Vec<JsonValue> = users
1858 .into_iter()
1859 .map(|u| {
1860 let mut obj = Map::new();
1861 obj.insert("username".into(), JsonValue::String(u.username));
1862 obj.insert("role".into(), JsonValue::String(u.role.as_str().into()));
1863 obj.insert("enabled".into(), JsonValue::Bool(u.enabled));
1864 obj.insert(
1865 "api_key_count".into(),
1866 JsonValue::Number(u.api_keys.len() as f64),
1867 );
1868 JsonValue::Object(obj)
1869 })
1870 .collect();
1871 json_to_string(&JsonValue::Array(arr))
1872 }
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877 use super::*;
1878 use std::io::{Read, Write};
1879 use std::net::{SocketAddr, TcpListener, TcpStream};
1880 use std::sync::atomic::{AtomicBool, Ordering};
1881 use std::sync::{Arc, Mutex};
1882 use std::thread::{self, JoinHandle};
1883 use std::time::Duration;
1884
1885 static ASK_ENV_LOCK: Mutex<()> = Mutex::new(());
1886
1887 fn make_server() -> McpServer {
1891 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
1892 McpServer::new(rt).with_capability(McpCapability::Admin)
1893 }
1894
1895 fn make_read_only_server() -> McpServer {
1897 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
1898 McpServer::new(rt)
1899 }
1900
1901 #[test]
1902 fn update_tool_renders_values_as_literals_and_refuses_crafted_identifiers() {
1903 let srv = make_server();
1904 srv.tool_query(&parse_json(
1905 r#"{"sql":"CREATE TABLE mcp_upd (id INTEGER, name TEXT)"}"#,
1906 ))
1907 .expect("create");
1908 srv.tool_query(&parse_json(
1909 r#"{"sql":"INSERT INTO mcp_upd (id, name) VALUES (1, 'a')"}"#,
1910 ))
1911 .expect("seed");
1912 srv.tool_update(&parse_json(
1915 r#"{"collection":"mcp_upd","set":{"name":"o'brien"},"where_filter":"id = 1"}"#,
1916 ))
1917 .expect("escaped value");
1918 let rows = srv
1919 .tool_query(&parse_json(
1920 r#"{"sql":"SELECT name FROM mcp_upd WHERE id = 1"}"#,
1921 ))
1922 .expect("read back");
1923 assert!(rows.contains("o'brien"), "{rows}");
1924
1925 for args in [
1926 r#"{"collection":"mcp_upd; DROP TABLE mcp_upd","set":{"name":"x"}}"#,
1927 r#"{"collection":"mcp_upd","set":{"name = 'x' WHERE 1=1 --":"x"}}"#,
1928 ] {
1929 let err = srv
1930 .tool_update(&parse_json(args))
1931 .expect_err("crafted identifier must be refused");
1932 assert!(err.contains("invalid"), "{err}");
1933 }
1934 }
1935
1936 #[test]
1937 fn read_only_session_refuses_mutating_tools() {
1938 let srv = make_read_only_server();
1942 assert_eq!(srv.capability(), McpCapability::ReadOnly);
1943 for tool in [
1944 "reddb_drop_collection",
1945 "reddb_delete",
1946 "reddb_insert_row",
1947 "reddb_vault_unseal",
1948 "reddb_auth_create_api_key",
1949 ] {
1950 let err = srv
1951 .check_tool_capability(tool)
1952 .expect_err("mutating tool must be refused by default");
1953 assert!(err.contains("read-only"), "{tool}: {err}");
1954 }
1955 srv.check_tool_capability("reddb_query")
1957 .expect("reads stay available");
1958 srv.check_tool_capability("reddb_collections")
1959 .expect("introspection stays available");
1960 }
1961
1962 #[test]
1963 fn read_only_session_refuses_mutating_sql_through_the_query_tool() {
1964 let srv = make_read_only_server();
1968 let err = srv
1969 .tool_query(&parse_json(r#"{"sql":"DROP TABLE users"}"#))
1970 .expect_err("DDL must be refused in a read-only session");
1971 assert!(err.contains("write"), "{err}");
1972
1973 let err = srv
1974 .tool_query(&parse_json(r#"{"sql":"SHOW SECRETS"}"#))
1975 .expect_err("secret reads must require admin");
1976 assert!(err.contains("admin"), "{err}");
1977
1978 srv.tool_query(&parse_json(r#"{"sql":"SELECT 1 AS one"}"#))
1979 .expect("plain reads still work");
1980 }
1981
1982 #[test]
1983 fn allow_write_does_not_unlock_vault_or_auth_tools() {
1984 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
1987 let srv = McpServer::new(rt).with_capability(McpCapability::Write);
1988 srv.check_tool_capability("reddb_insert_row")
1989 .expect("writes are enabled");
1990 let err = srv
1991 .check_tool_capability("reddb_vault_get")
1992 .expect_err("vault stays behind --allow-admin");
1993 assert!(err.contains("admin"), "{err}");
1994 }
1995
1996 fn parse_json(s: &str) -> JsonValue {
1997 json_from_str(s).expect("valid json")
1998 }
1999
2000 #[test]
2001 fn initialize_advertises_resources_capability() {
2002 let mut srv = make_server();
2003 let response = srv.handle_initialize(Some(&JsonValue::Number(1.0)));
2004 let parsed = parse_json(&response);
2005 let resources = parsed
2006 .get("result")
2007 .and_then(|result| result.get("capabilities"))
2008 .and_then(|caps| caps.get("resources"))
2009 .expect("resources capability advertised");
2010 assert_eq!(
2011 resources.get("subscribe").and_then(JsonValue::as_bool),
2012 Some(false)
2013 );
2014 }
2015
2016 #[test]
2017 fn resources_list_includes_rql_knowledge() {
2018 let mut srv = make_server();
2019 let request = parse_json(r#"{"jsonrpc":"2.0","id":7,"method":"resources/list"}"#);
2020 let response = srv.handle_message(&request).expect("response");
2021 let parsed = parse_json(&response);
2022 let resources = parsed
2023 .get("result")
2024 .and_then(|result| result.get("resources"))
2025 .and_then(JsonValue::as_array)
2026 .expect("resources array");
2027
2028 let rql = resources
2029 .iter()
2030 .find(|res| res.get("uri").and_then(JsonValue::as_str) == Some("reddb://knowledge/rql"))
2031 .expect("rql knowledge resource listed");
2032 assert_eq!(
2033 rql.get("mimeType").and_then(JsonValue::as_str),
2034 Some("text/markdown")
2035 );
2036 assert!(rql.get("name").and_then(JsonValue::as_str).is_some());
2037 }
2038
2039 #[test]
2040 fn resources_list_includes_connection_knowledge() {
2041 let mut srv = make_server();
2042 let request = parse_json(r#"{"jsonrpc":"2.0","id":11,"method":"resources/list"}"#);
2043 let response = srv.handle_message(&request).expect("response");
2044 let parsed = parse_json(&response);
2045 let resources = parsed
2046 .get("result")
2047 .and_then(|result| result.get("resources"))
2048 .and_then(JsonValue::as_array)
2049 .expect("resources array");
2050
2051 let connection = resources
2052 .iter()
2053 .find(|res| {
2054 res.get("uri").and_then(JsonValue::as_str)
2055 == Some(reddb_wire::knowledge::RESOURCE_URI)
2056 })
2057 .expect("connection knowledge resource listed");
2058 assert_eq!(
2059 connection.get("mimeType").and_then(JsonValue::as_str),
2060 Some("text/markdown")
2061 );
2062 assert!(connection.get("name").and_then(JsonValue::as_str).is_some());
2063 }
2064
2065 #[test]
2066 fn resources_read_returns_generated_rql_reference() {
2067 let mut srv = make_server();
2068 let request = parse_json(
2069 r#"{"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri":"reddb://knowledge/rql"}}"#,
2070 );
2071 let response = srv.handle_message(&request).expect("response");
2072 let parsed = parse_json(&response);
2073 let text = parsed
2074 .get("result")
2075 .and_then(|result| result.get("contents"))
2076 .and_then(JsonValue::as_array)
2077 .and_then(|contents| contents.first())
2078 .and_then(|item| item.get("text"))
2079 .and_then(JsonValue::as_str)
2080 .expect("resource text");
2081
2082 assert_eq!(text, reddb_rql::knowledge::rql_reference_markdown());
2085 assert!(text.contains("`SELECT`"), "keyword missing: {text:.120}");
2088 assert!(text.contains("`COUNT`"), "function missing");
2089 }
2090
2091 #[test]
2092 fn resources_read_returns_generated_type_reference() {
2093 let mut srv = make_server();
2094 let request = parse_json(
2095 r#"{"jsonrpc":"2.0","id":10,"method":"resources/read","params":{"uri":"reddb://knowledge/types"}}"#,
2096 );
2097 let response = srv.handle_message(&request).expect("response");
2098 let parsed = parse_json(&response);
2099 let text = parsed
2100 .get("result")
2101 .and_then(|result| result.get("contents"))
2102 .and_then(JsonValue::as_array)
2103 .and_then(|contents| contents.first())
2104 .and_then(|item| item.get("text"))
2105 .and_then(JsonValue::as_str)
2106 .expect("resource text");
2107
2108 assert_eq!(text, reddb_types::knowledge::type_reference_markdown());
2111 assert!(
2114 text.contains("`INTEGER`"),
2115 "value type missing: {text:.120}"
2116 );
2117 assert!(text.contains("Multi-model map"), "multi-model map missing");
2118 }
2119
2120 #[test]
2121 fn resources_read_returns_generated_connection_reference() {
2122 let mut srv = make_server();
2123 let request = parse_json(
2124 r#"{"jsonrpc":"2.0","id":12,"method":"resources/read","params":{"uri":"reddb://knowledge/connections"}}"#,
2125 );
2126 let response = srv.handle_message(&request).expect("response");
2127 let parsed = parse_json(&response);
2128 let text = parsed
2129 .get("result")
2130 .and_then(|result| result.get("contents"))
2131 .and_then(JsonValue::as_array)
2132 .and_then(|contents| contents.first())
2133 .and_then(|item| item.get("text"))
2134 .and_then(JsonValue::as_str)
2135 .expect("resource text");
2136
2137 assert_eq!(text, reddb_wire::knowledge::connection_reference_markdown());
2140 assert!(text.contains("`red://`"), "red scheme missing: {text:.120}");
2141 assert!(text.contains("`reds://`"), "reds scheme missing");
2142 assert!(
2143 text.contains("principal transport"),
2144 "principal transport narrative missing"
2145 );
2146 assert!(
2147 text.contains("Embedded / standalone"),
2148 "embedded topology missing"
2149 );
2150 assert!(text.contains("Serverless"), "serverless topology missing");
2151 assert!(
2152 text.contains("Primary-replica"),
2153 "primary-replica topology missing"
2154 );
2155 assert!(text.contains("Cluster"), "cluster topology missing");
2156 }
2157
2158 #[test]
2159 fn resources_read_unknown_uri_errors() {
2160 let mut srv = make_server();
2161 let request = parse_json(
2162 r#"{"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"reddb://knowledge/nope"}}"#,
2163 );
2164 let response = srv.handle_message(&request).expect("response");
2165 let parsed = parse_json(&response);
2166 assert!(
2167 parsed.get("error").is_some(),
2168 "unknown resource must error: {response}"
2169 );
2170 }
2171
2172 #[test]
2173 fn tools_list_registers_reddb_ask_descriptor() {
2174 let srv = make_server();
2175 let response = srv.handle_tools_list(Some(&JsonValue::Number(1.0)));
2176 let parsed = parse_json(&response);
2177 let tools = parsed
2178 .get("result")
2179 .and_then(|result| result.get("tools"))
2180 .and_then(JsonValue::as_array)
2181 .expect("tools array");
2182
2183 let ask = tools
2184 .iter()
2185 .find(|tool| tool.get("name").and_then(JsonValue::as_str) == Some("reddb.ask"))
2186 .expect("reddb.ask registered");
2187
2188 let desc = ask
2189 .get("description")
2190 .and_then(JsonValue::as_str)
2191 .expect("description");
2192 assert!(desc.contains("citations"), "description: {desc}");
2193 assert!(desc.contains("sources_flat"), "description: {desc}");
2194 assert!(desc.contains("URN"), "description: {desc}");
2195
2196 let options = ask
2197 .get("inputSchema")
2198 .and_then(|schema| schema.get("properties"))
2199 .and_then(|props| props.get("options"))
2200 .and_then(|opts| opts.get("properties"))
2201 .and_then(JsonValue::as_object)
2202 .expect("options properties");
2203 for key in [
2204 "strict",
2205 "using",
2206 "model",
2207 "limit",
2208 "min_score",
2209 "depth",
2210 "temperature",
2211 "seed",
2212 "cache",
2213 "nocache",
2214 ] {
2215 assert!(
2216 options.contains_key(key),
2217 "missing option {key} in {options:?}"
2218 );
2219 }
2220 }
2221
2222 #[test]
2223 fn embedded_dispatch_reaches_every_advertised_tool() {
2224 let server = make_server();
2225 let mut tool_names: Vec<&str> = tools::all_tools()
2226 .into_iter()
2227 .map(|tool| tool.name)
2228 .collect();
2229 tool_names.push(crate::runtime::ai::mcp_ask_tool::TOOL_NAME);
2230
2231 for tool_name in tool_names {
2232 let mut params = Map::new();
2233 params.insert("name".to_string(), JsonValue::String(tool_name.to_string()));
2234 params.insert("arguments".to_string(), JsonValue::Object(Map::new()));
2235 let response = server.handle_tools_call(
2236 Some(&JsonValue::Number(1.0)),
2237 Some(&JsonValue::Object(params)),
2238 );
2239 let parsed = parse_json(&response);
2240 let result = parsed.get("result").expect("tool result");
2241 let text = result
2242 .get("content")
2243 .and_then(JsonValue::as_array)
2244 .and_then(|content| content.first())
2245 .and_then(|item| item.get("text"))
2246 .and_then(JsonValue::as_str)
2247 .expect("tool result text");
2248
2249 assert!(
2250 !text.contains("unknown tool") && !text.contains("denied by policy"),
2251 "embedded tool {tool_name} did not reach its dispatch arm: {text}"
2252 );
2253 }
2254 }
2255
2256 #[test]
2257 fn tools_call_reddb_ask_uses_typed_argument_parser() {
2258 let srv = make_server();
2259 let params = parse_json(
2260 r#"{
2261 "name": "reddb.ask",
2262 "arguments": {
2263 "question": "what cites this?",
2264 "options": { "tempurature": 0.2 }
2265 }
2266 }"#,
2267 );
2268
2269 let response = srv.handle_tools_call(Some(&JsonValue::Number(1.0)), Some(¶ms));
2270 let parsed = parse_json(&response);
2271 let result = parsed.get("result").expect("result");
2272 assert_eq!(
2273 result.get("isError").and_then(JsonValue::as_bool),
2274 Some(true)
2275 );
2276 let text = result
2277 .get("content")
2278 .and_then(JsonValue::as_array)
2279 .and_then(|content| content.first())
2280 .and_then(|item| item.get("text"))
2281 .and_then(JsonValue::as_str)
2282 .expect("error text");
2283 assert!(text.contains("options.tempurature"), "text: {text}");
2284 }
2285
2286 fn call_type_of(srv: &mut McpServer, arguments: &str) -> JsonValue {
2290 let request = parse_json(&format!(
2291 r#"{{"jsonrpc":"2.0","id":21,"method":"tools/call","params":{{"name":"reddb_type_of","arguments":{arguments}}}}}"#
2292 ));
2293 let response = srv.handle_message(&request).expect("response");
2294 parse_json(&response)
2295 .get("result")
2296 .cloned()
2297 .expect("result object")
2298 }
2299
2300 fn type_of_result_json(result: &JsonValue) -> JsonValue {
2302 assert_ne!(
2303 result.get("isError").and_then(JsonValue::as_bool),
2304 Some(true),
2305 "tool returned an error: {result:?}"
2306 );
2307 let text = result
2308 .get("content")
2309 .and_then(JsonValue::as_array)
2310 .and_then(|content| content.first())
2311 .and_then(|item| item.get("text"))
2312 .and_then(JsonValue::as_str)
2313 .expect("result text");
2314 parse_json(text)
2315 }
2316
2317 #[test]
2318 fn tools_list_includes_reddb_type_of() {
2319 let srv = make_server();
2320 let response = srv.handle_tools_list(Some(&JsonValue::Number(1.0)));
2321 let parsed = parse_json(&response);
2322 let tools = parsed
2323 .get("result")
2324 .and_then(|result| result.get("tools"))
2325 .and_then(JsonValue::as_array)
2326 .expect("tools array");
2327 assert!(
2328 tools
2329 .iter()
2330 .any(|tool| tool.get("name").and_then(JsonValue::as_str) == Some("reddb_type_of")),
2331 "reddb_type_of must be advertised in tools/list"
2332 );
2333 }
2334
2335 #[test]
2336 fn tools_call_type_of_resolves_type_name_from_catalog() {
2337 let mut srv = make_server();
2338 let result = call_type_of(&mut srv, r#"{"type":"int"}"#);
2339 let facts = type_of_result_json(&result);
2340
2341 assert_eq!(
2343 facts.get("canonical_type").and_then(JsonValue::as_str),
2344 Some("INTEGER")
2345 );
2346 assert_eq!(
2347 facts.get("category").and_then(JsonValue::as_str),
2348 Some("Numeric")
2349 );
2350 let casts = facts.get("casts").and_then(JsonValue::as_array).unwrap();
2352 assert!(
2353 casts.iter().any(|c| {
2354 c.get("target").and_then(JsonValue::as_str) == Some("FLOAT")
2355 && c.get("context").and_then(JsonValue::as_str) == Some("implicit")
2356 }),
2357 "casts: {casts:?}"
2358 );
2359 let operators = facts
2361 .get("operators")
2362 .and_then(JsonValue::as_array)
2363 .unwrap();
2364 assert!(
2365 operators
2366 .iter()
2367 .any(|o| o.get("symbol").and_then(JsonValue::as_str) == Some("+")),
2368 "operators: {operators:?}"
2369 );
2370 }
2371
2372 #[test]
2373 fn tools_call_type_of_infers_type_from_literal_value() {
2374 let mut srv = make_server();
2375 let result = call_type_of(&mut srv, r#"{"value":true}"#);
2376 let facts = type_of_result_json(&result);
2377 assert_eq!(
2378 facts.get("canonical_type").and_then(JsonValue::as_str),
2379 Some("BOOLEAN")
2380 );
2381 }
2382
2383 #[test]
2384 fn tools_call_type_of_unknown_name_is_error() {
2385 let mut srv = make_server();
2386 let result = call_type_of(&mut srv, r#"{"type":"frobnicate"}"#);
2387 assert_eq!(
2388 result.get("isError").and_then(JsonValue::as_bool),
2389 Some(true)
2390 );
2391 let text = result
2392 .get("content")
2393 .and_then(JsonValue::as_array)
2394 .and_then(|content| content.first())
2395 .and_then(|item| item.get("text"))
2396 .and_then(JsonValue::as_str)
2397 .expect("error text");
2398 assert!(text.contains("frobnicate"), "text: {text}");
2399 }
2400
2401 #[test]
2402 fn tools_call_type_of_requires_an_argument() {
2403 let mut srv = make_server();
2404 let result = call_type_of(&mut srv, r#"{}"#);
2405 assert_eq!(
2406 result.get("isError").and_then(JsonValue::as_bool),
2407 Some(true)
2408 );
2409 }
2410
2411 #[test]
2412 fn tools_call_reddb_ask_returns_canonical_citation_envelope() {
2413 let _guard = ASK_ENV_LOCK.lock().expect("env lock");
2414 let stub = AskStub::start();
2415 let _api_base = EnvVarGuard::set(
2416 "REDDB_OLLAMA_API_BASE",
2417 &format!("http://{}/v1", stub.addr()),
2418 );
2419
2420 let srv = make_server();
2421 srv.tool_query(&parse_json(
2422 r#"{"sql":"CREATE TABLE travel (id INTEGER, passport TEXT, notes TEXT)"}"#,
2423 ))
2424 .expect("ddl ok");
2425 srv.tool_query(&parse_json(
2426 r#"{"sql":"INSERT INTO travel (id, passport, notes) VALUES (1, 'PT-002', 'incident FDD-12313 escalated')"}"#,
2427 ))
2428 .expect("insert ok");
2429
2430 let params = parse_json(
2431 r#"{
2432 "name": "reddb.ask",
2433 "arguments": {
2434 "question": "passport FDD-12313",
2435 "options": {
2436 "strict": false,
2437 "using": "ollama",
2438 "model": "mock-ask",
2439 "limit": 1,
2440 "min_score": 0,
2441 "depth": 0,
2442 "temperature": 0,
2443 "seed": 0,
2444 "cache": { "ttl": "5m" }
2445 }
2446 }
2447 }"#,
2448 );
2449
2450 let response = srv.handle_tools_call(Some(&JsonValue::Number(1.0)), Some(¶ms));
2451 let parsed = parse_json(&response);
2452 let result = parsed.get("result").expect("result");
2453 assert_ne!(
2454 result.get("isError").and_then(JsonValue::as_bool),
2455 Some(true),
2456 "response: {response}"
2457 );
2458 let text = result
2459 .get("content")
2460 .and_then(JsonValue::as_array)
2461 .and_then(|content| content.first())
2462 .and_then(|item| item.get("text"))
2463 .and_then(JsonValue::as_str)
2464 .expect("tool text");
2465 let envelope = parse_json(text);
2466
2467 assert_eq!(
2468 envelope.get("answer").and_then(JsonValue::as_str),
2469 Some("FDD-12313 escalated [^1].")
2470 );
2471 assert_eq!(
2472 envelope.get("provider").and_then(JsonValue::as_str),
2473 Some("ollama")
2474 );
2475 assert_eq!(
2476 envelope.get("model").and_then(JsonValue::as_str),
2477 Some("mock-ask")
2478 );
2479 assert_eq!(
2480 envelope.get("cache_hit").and_then(JsonValue::as_bool),
2481 Some(false)
2482 );
2483 assert!(envelope
2484 .get("sources_flat")
2485 .and_then(JsonValue::as_array)
2486 .is_some());
2487 assert!(envelope
2488 .get("citations")
2489 .and_then(JsonValue::as_array)
2490 .is_some());
2491 assert!(envelope
2492 .get("validation")
2493 .and_then(JsonValue::as_object)
2494 .is_some());
2495 assert!(
2496 envelope.get("rows").is_none(),
2497 "ASK must not be row-wrapped: {text}"
2498 );
2499 }
2500
2501 #[test]
2502 fn tool_query_without_params_keeps_legacy_path() {
2503 let srv = make_server();
2504 let args = parse_json(r#"{"sql":"SELECT 1 AS one"}"#);
2505 let out = srv.tool_query(&args).expect("query ok");
2506 assert!(out.contains("\"one\""), "expected 'one' column in {out}");
2507 }
2508
2509 #[test]
2510 fn tool_query_binds_int_and_text_params() {
2511 let srv = make_server();
2512 srv.tool_query(&parse_json(
2513 r#"{"sql":"CREATE TABLE mcpp (id INTEGER, name TEXT)"}"#,
2514 ))
2515 .expect("ddl ok");
2516 srv.tool_query(&parse_json(
2517 r#"{"sql":"INSERT INTO mcpp (id, name) VALUES (1, 'Alice')"}"#,
2518 ))
2519 .expect("insert 1");
2520 srv.tool_query(&parse_json(
2521 r#"{"sql":"INSERT INTO mcpp (id, name) VALUES (2, 'Bob')"}"#,
2522 ))
2523 .expect("insert 2");
2524
2525 let out = srv
2526 .tool_query(&parse_json(
2527 r#"{"sql":"SELECT * FROM mcpp WHERE id = $1 AND name = $2","params":[1,"Alice"]}"#,
2528 ))
2529 .expect("query with params ok");
2530 assert!(out.contains("Alice"), "expected Alice in {out}");
2531 assert!(!out.contains("Bob"), "Bob must not match: {out}");
2532 }
2533
2534 #[test]
2535 fn tool_query_params_must_be_array() {
2536 let srv = make_server();
2537 let err = srv
2538 .tool_query(&parse_json(
2539 r#"{"sql":"SELECT 1","params":{"not":"array"}}"#,
2540 ))
2541 .expect_err("must reject non-array params");
2542 assert!(err.contains("array"), "got {err}");
2543 }
2544
2545 #[test]
2546 fn tool_query_param_arity_mismatch_surfaces_error() {
2547 let srv = make_server();
2548 srv.tool_query(&parse_json(r#"{"sql":"CREATE TABLE mcpa (id INTEGER)"}"#))
2549 .expect("ddl ok");
2550 let err = srv
2551 .tool_query(&parse_json(
2552 r#"{"sql":"SELECT * FROM mcpa WHERE id = $1","params":[1,2]}"#,
2553 ))
2554 .expect_err("arity mismatch");
2555 assert!(
2556 err.contains("number of parameters") || err.contains("expects"),
2557 "got {err}"
2558 );
2559 }
2560
2561 #[test]
2562 fn tool_query_vector_param_binds_into_search_similar() {
2563 let srv = make_server();
2564 let out = srv.tool_query(&parse_json(
2565 r#"{"sql":"SEARCH SIMILAR $1 COLLECTION mcpv LIMIT 5","params":[[0.1,0.2,0.3]]}"#,
2566 ));
2567 if let Err(e) = out {
2571 assert!(
2572 !e.contains("placeholder") && !e.contains("Parameter"),
2573 "param did not bind: {e}"
2574 );
2575 }
2576 }
2577
2578 fn call_tool(srv: &McpServer, name: &str, rql: &str) -> JsonValue {
2584 let params = parse_json(
2585 &json_to_string(&{
2586 let mut p = Map::new();
2587 p.insert("name".to_string(), JsonValue::String(name.to_string()));
2588 let mut args = Map::new();
2589 args.insert("rql".to_string(), JsonValue::String(rql.to_string()));
2590 p.insert("arguments".to_string(), JsonValue::Object(args));
2591 JsonValue::Object(p)
2592 })
2593 .expect("serialize params"),
2594 );
2595 let response = srv.handle_tools_call(Some(&JsonValue::Number(1.0)), Some(¶ms));
2596 parse_json(&response)
2597 }
2598
2599 fn tool_result_text(parsed: &JsonValue) -> String {
2600 parsed
2601 .get("result")
2602 .and_then(|r| r.get("content"))
2603 .and_then(JsonValue::as_array)
2604 .and_then(|content| content.first())
2605 .and_then(|item| item.get("text"))
2606 .and_then(JsonValue::as_str)
2607 .expect("tool text")
2608 .to_string()
2609 }
2610
2611 #[test]
2612 fn tools_list_registers_rql_validate_and_explain() {
2613 let srv = make_server();
2614 let response = srv.handle_tools_list(Some(&JsonValue::Number(1.0)));
2615 let parsed = parse_json(&response);
2616 let tools = parsed
2617 .get("result")
2618 .and_then(|result| result.get("tools"))
2619 .and_then(JsonValue::as_array)
2620 .expect("tools array");
2621 let names: Vec<&str> = tools
2622 .iter()
2623 .filter_map(|t| t.get("name").and_then(JsonValue::as_str))
2624 .collect();
2625 assert!(names.contains(&"reddb_rql_validate"), "{names:?}");
2626 assert!(names.contains(&"reddb_rql_explain"), "{names:?}");
2627 }
2628
2629 #[test]
2630 fn tools_call_rql_validate_accepts_valid_query() {
2631 let srv = make_server();
2632 let parsed = call_tool(&srv, "reddb_rql_validate", "SELECT * FROM users");
2633 let result = parsed.get("result").expect("result");
2634 assert_ne!(
2635 result.get("isError").and_then(JsonValue::as_bool),
2636 Some(true)
2637 );
2638 let envelope = parse_json(&tool_result_text(&parsed));
2639 assert_eq!(
2640 envelope.get("valid").and_then(JsonValue::as_bool),
2641 Some(true)
2642 );
2643 assert_eq!(
2644 envelope.get("statement").and_then(JsonValue::as_str),
2645 Some("Table")
2646 );
2647 }
2648
2649 #[test]
2650 fn tools_call_rql_validate_reports_structured_error() {
2651 let srv = make_server();
2652 let parsed = call_tool(&srv, "reddb_rql_validate", "SELECT * FROM");
2653 let envelope = parse_json(&tool_result_text(&parsed));
2654 assert_eq!(
2655 envelope.get("valid").and_then(JsonValue::as_bool),
2656 Some(false)
2657 );
2658 let error = envelope.get("error").expect("structured error");
2659 assert!(error
2660 .get("message")
2661 .and_then(JsonValue::as_str)
2662 .is_some_and(|m| !m.is_empty()));
2663 assert!(error.get("line").and_then(JsonValue::as_f64).is_some());
2664 assert!(error
2665 .get("kind")
2666 .and_then(JsonValue::as_str)
2667 .is_some_and(|k| !k.is_empty()));
2668 }
2669
2670 #[test]
2671 fn tools_call_rql_explain_returns_ast_and_plan() {
2672 let srv = make_server();
2673 let parsed = call_tool(
2674 &srv,
2675 "reddb_rql_explain",
2676 "SELECT * FROM users WHERE id = 1",
2677 );
2678 let envelope = parse_json(&tool_result_text(&parsed));
2679 assert_eq!(
2680 envelope
2681 .get("validation")
2682 .and_then(|v| v.get("valid"))
2683 .and_then(JsonValue::as_bool),
2684 Some(true)
2685 );
2686 assert!(envelope.get("ast").and_then(JsonValue::as_str).is_some());
2687 assert!(envelope
2688 .get("optimized_ast")
2689 .and_then(JsonValue::as_str)
2690 .is_some());
2691 assert!(envelope
2692 .get("applied_passes")
2693 .and_then(JsonValue::as_array)
2694 .is_some());
2695 }
2696
2697 #[test]
2698 fn tools_call_rql_validate_missing_field_errors() {
2699 let srv = make_server();
2700 let params = parse_json(r#"{"name":"reddb_rql_validate","arguments":{}}"#);
2701 let response = srv.handle_tools_call(Some(&JsonValue::Number(1.0)), Some(¶ms));
2702 let parsed = parse_json(&response);
2703 let result = parsed.get("result").expect("result");
2704 assert_eq!(
2705 result.get("isError").and_then(JsonValue::as_bool),
2706 Some(true)
2707 );
2708 }
2709
2710 fn call_explain_connection(srv: &mut McpServer, uri: &str) -> JsonValue {
2711 let request = parse_json(
2712 &json_to_string(&{
2713 let mut request = Map::new();
2714 request.insert("jsonrpc".to_string(), JsonValue::String("2.0".to_string()));
2715 request.insert("id".to_string(), JsonValue::Number(31.0));
2716 request.insert(
2717 "method".to_string(),
2718 JsonValue::String("tools/call".to_string()),
2719 );
2720
2721 let mut args = Map::new();
2722 args.insert("uri".to_string(), JsonValue::String(uri.to_string()));
2723
2724 let mut params = Map::new();
2725 params.insert(
2726 "name".to_string(),
2727 JsonValue::String("reddb_explain_connection".to_string()),
2728 );
2729 params.insert("arguments".to_string(), JsonValue::Object(args));
2730 request.insert("params".to_string(), JsonValue::Object(params));
2731 JsonValue::Object(request)
2732 })
2733 .expect("serialize request"),
2734 );
2735 let response = srv.handle_message(&request).expect("response");
2736 let parsed = parse_json(&response);
2737 let result = parsed.get("result").expect("result");
2738 assert_ne!(
2739 result.get("isError").and_then(JsonValue::as_bool),
2740 Some(true),
2741 "tool returned error: {parsed:?}"
2742 );
2743 parse_json(&tool_result_text(&parsed))
2744 }
2745
2746 #[test]
2747 fn tools_call_explain_connection_maps_supported_schemes() {
2748 let mut srv = make_server();
2749 for (uri, mode, transport, topology) in [
2750 ("memory://", "embedded", "memory", "ephemeral"),
2751 (
2752 "file:///tmp/reddb/data.rdb",
2753 "embedded",
2754 "filesystem",
2755 "persisted",
2756 ),
2757 ("red://db.example:5050", "remote", "redwire", "single"),
2758 ("reds://db.example", "remote", "redwire_tls", "single"),
2759 ("grpc://db.example", "remote", "grpc", "single"),
2760 ("http://db.example", "remote", "http", "single"),
2761 ("https://db.example", "remote", "https", "single"),
2762 ] {
2763 let envelope = call_explain_connection(&mut srv, uri);
2764 assert_eq!(envelope.get("uri").and_then(JsonValue::as_str), Some(uri));
2765 assert_eq!(
2766 envelope.get("mode").and_then(JsonValue::as_str),
2767 Some(mode),
2768 "{uri}: {envelope:?}"
2769 );
2770 assert_eq!(
2771 envelope.get("transport").and_then(JsonValue::as_str),
2772 Some(transport),
2773 "{uri}: {envelope:?}"
2774 );
2775 assert_eq!(
2776 envelope.get("topology").and_then(JsonValue::as_str),
2777 Some(topology),
2778 "{uri}: {envelope:?}"
2779 );
2780 }
2781
2782 let cluster = call_explain_connection(&mut srv, "grpc://primary,replica?route=primary");
2783 assert_eq!(
2784 cluster.get("topology").and_then(JsonValue::as_str),
2785 Some("primary_replica")
2786 );
2787 assert_eq!(
2788 cluster.get("transport").and_then(JsonValue::as_str),
2789 Some("grpc")
2790 );
2791 assert_eq!(
2792 cluster
2793 .get("target")
2794 .and_then(|target| target.get("force_primary"))
2795 .and_then(JsonValue::as_bool),
2796 Some(true)
2797 );
2798 }
2799
2800 struct EnvVarGuard {
2801 name: &'static str,
2802 previous: Option<String>,
2803 }
2804
2805 impl EnvVarGuard {
2806 fn set(name: &'static str, value: &str) -> Self {
2807 let previous = std::env::var(name).ok();
2808 std::env::set_var(name, value);
2809 Self { name, previous }
2810 }
2811 }
2812
2813 impl Drop for EnvVarGuard {
2814 fn drop(&mut self) {
2815 if let Some(value) = self.previous.take() {
2816 std::env::set_var(self.name, value);
2817 } else {
2818 std::env::remove_var(self.name);
2819 }
2820 }
2821 }
2822
2823 struct AskStub {
2824 addr: SocketAddr,
2825 shutdown: Arc<AtomicBool>,
2826 handle: Option<JoinHandle<()>>,
2827 }
2828
2829 impl AskStub {
2830 fn start() -> Self {
2831 let listener = TcpListener::bind("127.0.0.1:0").expect("stub bind");
2832 listener
2833 .set_nonblocking(true)
2834 .expect("nonblocking listener");
2835 let addr = listener.local_addr().expect("local addr");
2836 let shutdown = Arc::new(AtomicBool::new(false));
2837 let server_shutdown = Arc::clone(&shutdown);
2838 let handle = thread::spawn(move || {
2839 while !server_shutdown.load(Ordering::Relaxed) {
2840 match listener.accept() {
2841 Ok((mut stream, _)) => {
2842 let request = read_stub_request(&mut stream);
2843 if request.contains("/embeddings") {
2844 write_json_response(
2845 &mut stream,
2846 r#"{"model":"mock-embedding","data":[{"index":0,"embedding":[1,0,0]}],"usage":{"prompt_tokens":3,"total_tokens":3}}"#,
2847 );
2848 } else {
2849 write_json_response(
2850 &mut stream,
2851 r#"{"model":"mock-ask","choices":[{"message":{"role":"assistant","content":"FDD-12313 escalated [^1]."},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":4,"total_tokens":14}}"#,
2852 );
2853 }
2854 }
2855 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
2856 thread::sleep(Duration::from_millis(1));
2857 }
2858 Err(_) => break,
2859 }
2860 }
2861 });
2862
2863 Self {
2864 addr,
2865 shutdown,
2866 handle: Some(handle),
2867 }
2868 }
2869
2870 fn addr(&self) -> SocketAddr {
2871 self.addr
2872 }
2873 }
2874
2875 impl Drop for AskStub {
2876 fn drop(&mut self) {
2877 self.shutdown.store(true, Ordering::Relaxed);
2878 let _ = TcpStream::connect(self.addr);
2879 if let Some(handle) = self.handle.take() {
2880 let _ = handle.join();
2881 }
2882 }
2883 }
2884
2885 fn read_stub_request(stream: &mut TcpStream) -> String {
2886 let _ = stream.set_read_timeout(Some(Duration::from_millis(100)));
2887 let mut buffer = [0_u8; 4096];
2888 let count = stream.read(&mut buffer).unwrap_or(0);
2889 String::from_utf8_lossy(&buffer[..count]).into_owned()
2890 }
2891
2892 fn write_json_response(stream: &mut TcpStream, body: &str) {
2893 let response = format!(
2894 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
2895 body.len()
2896 );
2897 stream
2898 .write_all(response.as_bytes())
2899 .expect("write stub response");
2900 }
2901}