use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process::{Child, ChildStdin, Command, Stdio};
use std::time::Duration;
fn send_request(
stdin: &mut ChildStdin,
reader: &mut BufReader<std::process::ChildStdout>,
request: &serde_json::Value,
) -> serde_json::Value {
let id = request.get("id").cloned();
let mut line = serde_json::to_string(request).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
loop {
let mut buf = String::new();
reader.read_line(&mut buf).expect("read_line failed");
let val: serde_json::Value =
serde_json::from_str(buf.trim()).expect("response is not valid JSON");
match &id {
None => return val,
Some(expected_id) => {
if val.get("id") == Some(expected_id) {
return val;
}
}
}
}
}
fn send_notification(stdin: &mut ChildStdin, notification: &serde_json::Value) {
let mut line = serde_json::to_string(notification).unwrap();
line.push('\n');
stdin.write_all(line.as_bytes()).unwrap();
stdin.flush().unwrap();
}
struct ChildGuard(Child);
impl Drop for ChildGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[test]
#[ignore = "requires standalone librarian binary which no longer exists post-dissolution into codescout crate"]
fn mcp_subprocess_integration() {
let tmp = tempfile::TempDir::new().unwrap();
let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/librarian/fixtures/repo_a");
assert!(
fixture_dir.exists(),
"fixture dir missing: {}",
fixture_dir.display()
);
let ws_path = tmp.path().join("workspace.toml");
let db_path = tmp.path().join("catalog.db");
let ws_toml = format!(
"[[roots]]\nname = \"repo_a\"\npath = \"{}\"\n\n[[rule]]\nglob = \"**/*.md\"\nkind = \"doc\"\n",
fixture_dir.display()
);
std::fs::write(&ws_path, &ws_toml).unwrap();
let reindex_status = Command::new(assert_cmd::cargo::cargo_bin("librarian-mcp"))
.arg("reindex")
.env("LIBRARIAN_WORKSPACE", &ws_path)
.env("LIBRARIAN_DB", &db_path)
.status()
.expect("failed to spawn librarian-mcp reindex");
assert!(
reindex_status.success(),
"reindex exited with non-zero status"
);
let mut child = Command::new(assert_cmd::cargo::cargo_bin("librarian-mcp"))
.env("LIBRARIAN_WORKSPACE", &ws_path)
.env("LIBRARIAN_DB", &db_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn librarian-mcp");
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let _guard = ChildGuard(child);
let init_req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "test", "version": "0.1" }
}
});
let init_resp = send_request(&mut stdin, &mut reader, &init_req);
assert_eq!(init_resp["id"], 1, "initialize response id mismatch");
assert!(
init_resp.get("result").is_some(),
"initialize: expected result, got: {init_resp}"
);
send_notification(
&mut stdin,
&serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}),
);
let tools_req = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
});
let tools_resp = send_request(&mut stdin, &mut reader, &tools_req);
assert_eq!(tools_resp["id"], 2, "tools/list response id mismatch");
let tools = tools_resp["result"]["tools"]
.as_array()
.expect("tools/list result.tools should be an array");
assert_eq!(
tools.len(),
15,
"expected 15 tools, got {}: {:?}",
tools.len(),
tools.iter().map(|t| &t["name"]).collect::<Vec<_>>()
);
let call_req = serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "artifact_find",
"arguments": {}
}
});
let call_resp = send_request(&mut stdin, &mut reader, &call_req);
assert_eq!(call_resp["id"], 3, "tools/call response id mismatch");
assert!(
call_resp.get("result").is_some(),
"artifact_find: expected result, got: {call_resp}"
);
let content = call_resp["result"]["content"]
.as_array()
.expect("result.content should be an array");
assert!(!content.is_empty(), "artifact_find returned empty content");
let text = content[0]["text"]
.as_str()
.expect("content[0].text should be a string");
let payload: serde_json::Value =
serde_json::from_str(text).expect("content[0].text is not valid JSON");
let count = payload["count"]
.as_u64()
.or_else(|| payload["count"].as_i64().map(|v| v as u64))
.expect("artifact_find result should have a numeric 'count' field");
assert!(
count >= 1,
"expected at least 1 artifact from fixture, got count={count}"
);
std::thread::sleep(Duration::from_millis(50));
}