//! Genuine executable MCP boundary: handshake, discovery, source scope and lifecycle. use serde_json::{json, Value}; use std::{ io::{BufRead, BufReader, Write}, process::{Child, ChildStdin, Command, Stdio}, sync::mpsc, time::Duration, }; struct Server { child: Child, input: ChildStdin, output: mpsc::Receiver, } impl Drop for Server { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } impl Server { fn start(db: &std::path::Path) -> Self { Self::start_with_assets(db, None) } fn start_with_assets( db: &std::path::Path, assets: Option<(&std::ffi::OsStr, &std::ffi::OsStr)>, ) -> Self { let mut command = Command::new(env!("CARGO_BIN_EXE_searchi")); command.args(["mcp", "--db"]).arg(db); if let Some((model, worker)) = assets { command .arg("--model-dir") .arg(model) .arg("--embedding-worker") .arg(worker); } let mut child = command .env("SEARCHI_FORCE_CPU", "1") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .unwrap(); let input = child.stdin.take().unwrap(); let stdout = child.stdout.take().unwrap(); let (tx, output) = mpsc::channel(); std::thread::spawn(move || { for line in BufReader::new(stdout).lines() { if tx.send(line.unwrap()).is_err() { break; } } }); Self { child, input, output, } } fn send(&mut self, value: Value) { writeln!(self.input, "{value}").unwrap(); self.input.flush().unwrap(); } fn request(&mut self, id: u32, method: &str, params: Value) -> Value { self.send(json!({"jsonrpc":"2.0","id":id,"method":method,"params":params})); loop { let line = self .output .recv_timeout(Duration::from_secs(15)) .expect("MCP response deadline"); let value: Value = serde_json::from_str(&line).expect("stdout contains non-JSONRPC logging"); assert_eq!(value["jsonrpc"], "2.0"); if value["id"] == id { return value; } } } } #[test] fn actual_mcp_handshake_search_and_guarded_sources() { let fixture = tempfile::tempdir().unwrap(); let folder = fixture.path().join("documents"); std::fs::create_dir(&folder).unwrap(); let file = folder.join("contract.txt"); std::fs::write( &file, "Termination clause: sixty days written notice to end the supplier partnership.", ) .unwrap(); std::fs::write( folder.join("retry.rs"), "pub fn refreshAccessToken() { /* retry requests */ }", ) .unwrap(); std::fs::write( folder.join("symbol-note.txt"), "refreshAccessToken retry requests", ) .unwrap(); let db = fixture.path().join("index.sqlite"); let indexed = Command::new(env!("CARGO_BIN_EXE_searchi")) .args(["index", "--db"]) .arg(&db) .arg("--root") .arg(&folder) .output() .unwrap(); assert!(indexed.status.success()); let mut server = Server::start(&db); let hello=server.request(1,"initialize",json!({"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"searchi-test","version":"1"}})); assert_eq!(hello["result"]["serverInfo"]["name"], "searchi"); server.send(json!({"jsonrpc":"2.0","method":"notifications/initialized"})); let tools = server.request(2, "tools/list", json!({})); let names: Vec<&str> = tools["result"]["tools"] .as_array() .unwrap() .iter() .map(|tool| { assert_eq!(tool["annotations"]["readOnlyHint"], true); tool["name"].as_str().unwrap() }) .collect(); assert_eq!(names.len(), 3); assert!( names.contains(&"search") && names.contains(&"stats") && names.contains(&"read_source") ); let found = server.request( 3, "tools/call", json!({"name":"search","arguments":{"query":"termination","limit":3,"extension":"txt"}}), ); let data = &found["result"]["structuredContent"]; assert_eq!(data["mode"], "lexical_only"); assert_eq!(data["main_result_count"], 1); assert_eq!(data["related_result_count"], 0); assert_eq!(data["match_assessment"], "main_matches"); assert_eq!(data["results"][0]["evidence"], "main_match"); assert_eq!(data["results"][0]["filename"], "contract.txt"); assert!(data["results"][0]["snippet"] .as_str() .unwrap() .contains("sixty")); let handle = data["results"][0]["source_handle"] .as_str() .unwrap() .to_owned(); let source = server.request( 4, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert!(source["result"]["structuredContent"]["text"] .as_str() .unwrap() .contains("sixty")); let invented = server.request( 5, "tools/call", json!({"name":"read_source","arguments":{"handle":"invented"}}), ); assert_eq!(invented["result"]["isError"], true); let invalid = server.request( 6, "tools/call", json!({"name":"search","arguments":{"query":"termination","limit":101}}), ); assert_eq!(invalid["result"]["isError"], true); for (offset, folder) in [r"\\host\share", r"\\?\UNC\host\share", "//host/share"] .iter() .enumerate() { let rejected = server.request( 20 + offset as u32, "tools/call", json!({"name":"search","arguments":{"query":"termination","folder":folder}}), ); assert_eq!(rejected["result"]["isError"], true); assert!(rejected["result"]["structuredContent"]["error"] .as_str() .unwrap() .contains("local path")); } let code = server.request( 40, "tools/call", json!({"name":"search","arguments":{"query":"refreshAccessToken","code_only":true}}), ); let code_results = code["result"]["structuredContent"]["results"] .as_array() .unwrap(); assert_eq!(code_results.len(), 1); assert_eq!(code_results[0]["filename"], "retry.rs"); assert_eq!(code_results[0]["source"]["kind"], "line"); assert!(code_results[0]["snippet"] .as_str() .unwrap() .contains("refreshAccessToken")); let mutation = server.request(7, "tools/call", json!({"name":"index","arguments":{}})); assert!(mutation["error"].is_object()); std::fs::write( &file, "The source changed and its previous clause is stale.", ) .unwrap(); let stale = server.request( 8, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert_eq!(stale["result"]["isError"], true); let reindexed = Command::new(env!("CARGO_BIN_EXE_searchi")) .args(["index", "--db"]) .arg(&db) .arg("--root") .arg(&folder) .output() .unwrap(); assert!(reindexed.status.success()); let replaced = server.request( 30, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert_eq!(replaced["result"]["isError"], true); assert!(replaced["result"]["structuredContent"]["error"] .as_str() .unwrap() .contains("version changed")); std::fs::remove_file(&file).unwrap(); let deleted = server.request( 9, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert_eq!(deleted["result"]["isError"], true); let stats = server.request(10, "tools/call", json!({"name":"stats","arguments":{}})); assert_eq!(stats["result"]["structuredContent"]["mode"], "lexical_only"); } #[test] fn missing_database_never_creates_file_or_protocol_noise() { let temp = tempfile::tempdir().unwrap(); let db = temp.path().join("missing.sqlite"); let output = Command::new(env!("CARGO_BIN_EXE_searchi")) .args(["mcp", "--db"]) .arg(&db) .output() .unwrap(); assert!(!output.status.success()); assert!(output.stdout.is_empty()); assert!(!db.exists()); } #[test] #[ignore = "Requires verified Google model and current CPU-capable native worker"] fn actual_google_cli_and_mcp_use_query_role_with_cited_results() { let models = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set models"); let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set worker"); let fixture = tempfile::tempdir().unwrap(); let folder = fixture.path().join("documents"); std::fs::create_dir(&folder).unwrap(); std::fs::write( folder.join("supplier.txt"), "The supplier agreement may be terminated with sixty days written notice.", ) .unwrap(); std::fs::write( folder.join("garden.txt"), "Vegetable beds and rainwater collection in the garden.", ) .unwrap(); std::fs::write( folder.join("retry.rs"), "pub fn retry_backoff(attempt: u32) -> u64 {\n 100 * 2_u64.pow(attempt)\n}\n", ) .unwrap(); let db = fixture.path().join("index.sqlite"); searchi_core::SearchIndex::open(&db) .unwrap() .index_folder(&folder) .unwrap(); let indexer = searchi_service::HybridSearch::open(&db, &models, &worker, 1).unwrap(); assert_eq!(indexer.worker_info().unwrap().role, "indexing"); drop(indexer); let embedded = Command::new(env!("CARGO_BIN_EXE_searchi")) .args(["embed", "--db"]) .arg(&db) .arg("--model-dir") .arg(&models) .arg("--embedding-worker") .arg(&worker) .arg("--text-only") .env("SEARCHI_FORCE_CPU", "1") .output() .unwrap(); assert!( embedded.status.success(), "{}", String::from_utf8_lossy(&embedded.stderr) ); let output = Command::new(env!("CARGO_BIN_EXE_searchi")) .args(["search", "--db"]) .arg(&db) .args(["cancel the supplier partnership", "--json"]) .arg("--model-dir") .arg(&models) .arg("--embedding-worker") .arg(&worker) .env("SEARCHI_FORCE_CPU", "1") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let cli: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(cli[0]["filename"], "supplier.txt"); let mut server = Server::start_with_assets(&db, Some((&models, &worker))); server.request(1,"initialize",json!({"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"query-role-regression","version":"1"}})); server.send(json!({"jsonrpc":"2.0","method":"notifications/initialized"})); let stats = server.request(2, "tools/call", json!({"name":"stats","arguments":{}})); let status = &stats["result"]["structuredContent"]; assert_eq!(status["worker"]["role"], "query"); assert_eq!(status["worker"]["context_tokens"], 512); assert_eq!(status["worker"]["device"], "cpu"); assert_eq!(status["worker"]["media_supported"], true); let found = server.request( 3, "tools/call", json!({"name":"search","arguments":{"query":"cancel the supplier partnership","limit":2}}), ); let first = &found["result"]["structuredContent"]["results"][0]; assert_eq!(first["filename"], "supplier.txt"); let handle = first["source_handle"].as_str().unwrap(); let source = server.request( 4, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert!(source["result"]["structuredContent"]["text"] .as_str() .unwrap() .contains("sixty days")); let code=server.request(5,"tools/call",json!({"name":"search","arguments":{"query":"retry failed requests with exponential backoff","code_only":true}})); let first = &code["result"]["structuredContent"]["results"][0]; assert_eq!(first["filename"], "retry.rs"); assert_eq!(first["source"]["kind"], "line"); let handle = first["source_handle"].as_str().unwrap(); let source = server.request( 6, "tools/call", json!({"name":"read_source","arguments":{"handle":handle}}), ); assert!(source["result"]["structuredContent"]["text"] .as_str() .unwrap() .contains("2_u64.pow")); }