Menu
AkurAI-Build
publicLatest change 1488f88860de09bbb6791f4c67d3a8a8a3e0765f - Replace the JSON CI CLI with a full read/write akurai-build MCP tool set (repo add/host/sync/rename/remove/visibility/branches/tree, run queue/wait/retry/promote, artifact get, doctor, init); keep keygen/migrate/serve as plain CLI subcommands; update skill and deploy.md to the MCP tool set by Ólafur Búi Ólafsson
#![allow(dead_code)]
//! Shared MCP stdio test client. Included per test binary via `#[path]`.
use std::{
io::{BufRead, BufReader, Write},
path::Path,
process::{Child, ChildStdin, Command, Stdio},
};
use anyhow::{Context, Result, ensure};
use serde_json::{Value, json};
pub struct McpSession {
child: Child,
stdin: ChildStdin,
reader: BufReader<std::process::ChildStdout>,
next_id: i64,
}
impl McpSession {
/// Spawn `akurai --data DATA --key-file KEY mcp`. The schema must already
/// be migrated. `native` fixes `AKURAI_ALLOW_NATIVE` for the process
/// lifetime, matching the server's own startup-time flag.
pub fn start(data: &Path, key: &Path, native: bool) -> Result<Self> {
let mut command = Command::new(env!("CARGO_BIN_EXE_akurai"));
command
.args(["--data", str(data)?, "--key-file", str(key)?, "mcp"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
if native {
command.env("AKURAI_ALLOW_NATIVE", "1");
} else {
command.env_remove("AKURAI_ALLOW_NATIVE");
}
let mut child = command.spawn()?;
let stdin = child.stdin.take().context("mcp stdin missing")?;
let stdout = child.stdout.take().context("mcp stdout missing")?;
Ok(Self {
child,
stdin,
reader: BufReader::new(stdout),
next_id: 1,
})
}
/// Call a tool expecting success and return its parsed JSON payload.
pub fn call(&mut self, name: &str, arguments: Value) -> Result<Value> {
let result = self.raw_call(name, arguments)?;
ensure!(
result["isError"] != true,
"tool {name} failed: {}",
result["content"][0]["text"]
);
let text = result["content"][0]["text"]
.as_str()
.context("missing tool text")?;
Ok(serde_json::from_str(text)?)
}
/// Call a tool expecting a tool-level error and return its message text.
pub fn call_error(&mut self, name: &str, arguments: Value) -> Result<String> {
let result = self.raw_call(name, arguments)?;
ensure!(
result["isError"] == true,
"tool {name} unexpectedly succeeded"
);
Ok(result["content"][0]["text"]
.as_str()
.context("missing tool text")?
.to_owned())
}
fn raw_call(&mut self, name: &str, arguments: Value) -> Result<Value> {
let id = self.next_id;
self.next_id += 1;
let request = json!({
"jsonrpc": "2.0",
"id": id,
"method": "tools/call",
"params": {"name": name, "arguments": arguments}
});
writeln!(self.stdin, "{request}")?;
self.stdin.flush()?;
let mut line = String::new();
self.reader.read_line(&mut line)?;
ensure!(!line.trim().is_empty(), "mcp server closed the connection");
let response: Value =
serde_json::from_str(&line).with_context(|| format!("invalid MCP response: {line}"))?;
ensure!(
response.get("error").is_none(),
"mcp rpc error: {}",
response["error"]
);
Ok(response["result"].clone())
}
}
impl Drop for McpSession {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
pub fn str(path: &Path) -> Result<&str> {
path.to_str().context("test path is not UTF-8")
}