AkurAI Build
Menu

AkurAI-Build

public

Latest 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")
}