Skip to main content

reddb_server/mcp/
server.rs

1//! MCP Server for RedDB.
2//!
3//! Runs an embedded RedDB runtime and exposes it to AI agents via the
4//! Model Context Protocol JSON-RPC transport over stdio.
5
6use 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
34/// MCP server wrapping an embedded RedDB runtime.
35pub struct McpServer {
36    runtime: RedDBRuntime,
37    auth_store: Arc<AuthStore>,
38    initialized: bool,
39    /// What this session may do. Read-only unless the operator opted in —
40    /// see [`crate::mcp::capability`] for why the tool list, not the
41    /// transport, is the boundary that matters here.
42    capability: McpCapability,
43}
44
45impl McpServer {
46    /// Create a new MCP server with the given runtime.
47    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    /// Set the session's capability level. The default is read-only; the
63    /// CLI raises it from `--allow-write` / `--allow-admin`.
64    pub fn with_capability(mut self, capability: McpCapability) -> Self {
65        self.capability = capability;
66        self
67    }
68
69    /// The session's capability level.
70    pub fn capability(&self) -> McpCapability {
71        self.capability
72    }
73
74    /// Refuse `tool_name` when the session is below the level it needs.
75    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    /// Run the MCP server reading from stdin and writing to stdout.
89    ///
90    /// This blocks until stdin is closed (EOF). Diagnostic messages are
91    /// written to stderr so they do not interfere with the protocol.
92    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    /// Route a JSON-RPC message to the appropriate handler.
134    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                // Notification -- no response required.
142                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    // ------------------------------------------------------------------
165    // MCP lifecycle
166    // ------------------------------------------------------------------
167
168    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            // Read-only knowledge resources (ADR 0061). No subscriptions and
179            // the set is static, so both change-notification flags are false.
180            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    // ------------------------------------------------------------------
208    // tools/list
209    // ------------------------------------------------------------------
210
211    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    // ------------------------------------------------------------------
234    // resources/list + resources/read (knowledge surface, ADR 0061)
235    // ------------------------------------------------------------------
236
237    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    // ------------------------------------------------------------------
300    // tools/call dispatcher
301    // ------------------------------------------------------------------
302
303    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    // ------------------------------------------------------------------
392    // Tool implementations
393    // ------------------------------------------------------------------
394
395    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        // `reddb_query` is a read-only *tool* that accepts arbitrary SQL, so
402        // the statement decides what it needs. Without this a read-only
403        // session could send `DROP COLLECTION` through the one tool it was
404        // allowed to call.
405        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        // Optional positional `$N` bind parameters. MCP, stdio, and HTTP
415        // all decode through the Request module's transport-neutral codec.
416        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            // Bind inside the runtime's statement frame (#2183) so a
429            // parameterized MCP query keeps the same snapshot isolation and
430            // `AS OF` resolution as textual SQL.
431            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        // Build UPDATE SQL and execute via runtime. The collection and the
1147        // column names are spliced as identifiers, so they must be bare
1148        // identifiers; the values go through the SQL literal renderer, which
1149        // escapes quotes and backslashes the way the lexer reads them.
1150        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    /// `reddb_type_of`: resolve a type name or a literal value to its canonical
1398    /// type plus applicable casts/operators, answered entirely from the
1399    /// generated `reddb-io-types` catalog (ADR 0061). This handler owns no type
1400    /// knowledge — it only marshals arguments into and JSON out of
1401    /// [`reddb_types::knowledge::type_of_json`].
1402    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
1428// ------------------------------------------------------------------
1429// Helpers
1430// ------------------------------------------------------------------
1431
1432fn 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
1605/// Render an RQL parse verdict (ADR 0061, #1317) as the JSON the
1606/// `reddb_rql_validate` / `reddb_rql_explain` tools return.
1607fn 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
1626/// Render a structured RQL parse diagnostic as JSON.
1627fn 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
1658/// Parse optional metadata from an `args` JSON object.
1659fn 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
1748// Convert a storage Value to JSON (local helper to avoid visibility issues).
1749fn 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
1755// Auth tool implementations
1756impl 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    /// The existing suite exercises the whole tool surface (DDL, inserts,
1888    /// vault, auth), so it runs at the level an operator would opt into with
1889    /// `--allow-admin`. `read_only_session_*` below cover the default.
1890    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    /// A session at the shipped default: no flags.
1896    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        // A quote inside the value used to close the literal and run the
1913        // remainder as SQL.
1914        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        // The MCP transport has no second principal to authenticate, so the
1939        // tool list is the boundary: a model steered by injected content
1940        // must not reach `drop_collection` or the vault by default.
1941        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        // Reads still work, otherwise the default is useless.
1956        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        // `reddb_query` is a read tool that takes arbitrary SQL, so the
1965        // statement has to be classified too — otherwise the one tool a
1966        // read-only session may call is a full write channel.
1967        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        // `--allow-write` is for data and schema. Credentials need the
1985        // separate, louder opt-in.
1986        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        // The body is exactly what the RQL authority generates — same source as
2083        // docs/llms.txt.
2084        assert_eq!(text, reddb_rql::knowledge::rql_reference_markdown());
2085        // And it is genuinely generated from the engine: a sampled keyword and
2086        // function from the authorities are present.
2087        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        // The body is exactly what the type authority generates — same source as
2109        // docs/llms.txt.
2110        assert_eq!(text, reddb_types::knowledge::type_reference_markdown());
2111        // And it is genuinely generated from the engine: a sampled value type and
2112        // a multi-model paradigm are present.
2113        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        // The body is exactly what the wire authority generates — same source as
2138        // docs/llms.txt.
2139        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(&params));
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    /// Helper: drive a `reddb_type_of` `tools/call` over the JSON-RPC seam and
2287    /// return the parsed result object (`content`/`isError`), exercising the
2288    /// real dispatcher.
2289    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    /// Extract the text payload of a (non-error) tool result and parse it as JSON.
2301    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        // Canonical type + category come from the catalog authority.
2342        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        // Applicable casts include the implicit INTEGER → FLOAT widening.
2351        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        // Applicable operators include arithmetic +.
2360        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(&params));
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        // The collection doesn't exist; we only need to confirm the
2568        // param-bind path runs (i.e. the error reflects runtime semantics,
2569        // not a `$N` placeholder being unresolved).
2570        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    // ------------------------------------------------------------------
2579    // RQL validate / explain tools (ADR 0061, #1317), exercised at the
2580    // JSON-RPC tools/call seam.
2581    // ------------------------------------------------------------------
2582
2583    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(&params));
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(&params));
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}