use std::path::Path;
use std::process::Command;
use rmcp::serve_client;
use rmcp::transport::{ConfigureCommandExt, TokioChildProcess};
use serde_json::{Value, json};
const PAD: &str = "Pad line for chunk boundary coverage\n";
fn write_fixtures(dir: &Path) {
let docs = dir.join("docs");
std::fs::create_dir_all(docs.join("meetings")).unwrap();
std::fs::write(
docs.join("readme.md"),
"# Project README\n\nThis is the main readme file for the project.\n\nIt contains important information about setup and usage.",
)
.unwrap();
std::fs::write(
docs.join("api.md"),
"# API Documentation\n\nThis document describes the REST API endpoints.\n\n## Authentication\n\nUse Bearer tokens for auth.",
)
.unwrap();
std::fs::write(
docs.join("meetings/meeting-2024-01.md"),
"# January Meeting Notes\n\nDiscussed Q1 goals and roadmap.",
)
.unwrap();
std::fs::write(
docs.join("meetings/meeting-2024-02.md"),
"# February Meeting Notes\n\nFollowed up on Q1 progress.",
)
.unwrap();
std::fs::write(
docs.join("large-file.md"),
format!("# Large Document\n\n{}", "Lorem ipsum ".repeat(2000)),
)
.unwrap();
let abs = format!(
"{}{}{}",
PAD.repeat(300),
"UNIQUE_KEYWORD_XYZ marker\n",
PAD.repeat(20)
);
std::fs::write(docs.join("absolute-line-fixture.md"), abs).unwrap();
}
fn call(name: &str, args: Value) -> rmcp::model::CallToolRequestParams {
serde_json::from_value(json!({ "name": name, "arguments": args })).unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn mcp_stdio_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let docs = root.join("docs");
let cfg = root.join("config");
let db = root.join("index.sqlite");
std::fs::create_dir_all(&cfg).unwrap();
std::fs::write(cfg.join("index.yml"), "collections: {}\n").unwrap();
write_fixtures(root);
let out = Command::new(env!("CARGO_BIN_EXE_rqmd"))
.args([
"--index",
"index",
"collection",
"add",
docs.to_str().unwrap(),
"--name",
"docs",
])
.env("RQMD_INDEX_PATH", &db)
.env("RQMD_CONFIG_DIR", &cfg)
.env("CI", "1")
.env("NO_COLOR", "1")
.env_remove("RUST_LOG")
.env_remove("XDG_CACHE_HOME")
.env_remove("XDG_CONFIG_HOME")
.env_remove("RQMD_CACHE_DIR")
.current_dir(root)
.output()
.expect("spawn `rqmd collection add`");
assert!(
out.status.success(),
"collection add failed\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let transport = TokioChildProcess::new(
tokio::process::Command::new(env!("CARGO_BIN_EXE_rqmd")).configure(|cmd| {
cmd.args(["--index", "index", "mcp"])
.env("RQMD_INDEX_PATH", &db)
.env("RQMD_CONFIG_DIR", &cfg)
.env("CI", "1")
.env("NO_COLOR", "1")
.env_remove("RUST_LOG")
.env_remove("XDG_CACHE_HOME")
.env_remove("XDG_CONFIG_HOME")
.env_remove("RQMD_CACHE_DIR")
.current_dir(root);
}),
)
.expect("spawn `rqmd mcp`");
let client = serve_client((), transport).await.expect("client connected");
let info = client.peer_info().expect("peer info");
assert_eq!(info.server_info.name, "rqmd");
let tools = client.list_all_tools().await.expect("list tools");
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
for expected in ["query", "get", "multi_get", "status"] {
assert!(names.contains(&expected), "missing tool {expected}");
}
let status = client
.call_tool(call("status", json!({})))
.await
.expect("status");
let sc = status.structured_content.expect("status structuredContent");
assert!(sc["totalDocuments"].as_i64().unwrap() >= 5);
let got = client
.call_tool(call("get", json!({ "file": "readme.md" })))
.await
.expect("get");
assert_eq!(got.is_error, Some(false));
let block = serde_json::to_value(&got.content[0]).unwrap();
assert_eq!(block["type"], "resource");
assert!(
block["resource"]["text"]
.as_str()
.unwrap()
.contains("Project README")
);
let q = client
.call_tool(call(
"query",
json!({ "searches": [{ "type": "lex", "query": "UNIQUE_KEYWORD_XYZ" }], "rerank": false }),
))
.await
.expect("query");
let results = q.structured_content.expect("query structuredContent");
let results = results["results"].as_array().expect("results array");
let hit = results
.iter()
.find(|r| r["file"] == "docs/absolute-line-fixture.md")
.expect("absolute-line hit");
assert_eq!(hit["line"], 301);
let first_line = hit["snippet"].as_str().unwrap().lines().next().unwrap();
assert!(
first_line.contains(": @@ -3"),
"unexpected snippet header: {first_line}"
);
client.cancel().await.ok();
}