Menu
AkurAI-Build
publicLatest change be7ac4fa1c7f5fd9cb2dfd08f32992c3ed67577d - Accept repository filter in akurai_runs by Ólafur Búi Ólafsson
use std::{
fs,
io::{self, BufRead, Write},
path::PathBuf,
process::Command,
thread,
time::{Duration, Instant},
};
use anyhow::{Context, Result, bail, ensure};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::{
config,
db::{Database, RepositoryQuery, RunQuery},
git_process, hosted_git, release,
runner::Runner,
tree::RepoTree,
};
const PROTOCOL_VERSION: &str = "2025-06-18";
/// Longest a tool call may block the single-threaded MCP loop. MCP clients
/// time out requests at ~30s, and every queued request waits behind the
/// current one, so a longer server-side wait can never be observed — it only
/// starves unrelated calls. Long-running work is executed by the serve
/// daemon's worker loop; wait-style tools poll the database and return the
/// latest run state when this budget elapses.
const MAX_BLOCKING_WAIT: Duration = Duration::from_secs(25);
/// Fixed context every tool call runs against: the encrypted database, the
/// data root used to resolve hosted mirrors/workspaces/artifacts, and
/// whether native (non-Docker) job execution is permitted.
struct Ctx {
database: Database,
root: PathBuf,
allow_native: bool,
tree: RepoTree,
}
impl Ctx {
fn runner(&self) -> Result<Runner> {
Runner::new(self.database.clone(), self.root.clone(), self.allow_native)
}
}
pub fn serve(database: Database, root: PathBuf, allow_native: bool) -> Result<()> {
let tree = RepoTree::new(&root)?;
let context = Ctx {
database,
root,
allow_native,
tree,
};
let stdin = io::stdin();
let mut stdout = io::stdout().lock();
for line in stdin.lock().lines() {
let line = line.context("read MCP request")?;
if line.trim().is_empty() {
continue;
}
let response = match serde_json::from_str::<Value>(&line) {
Ok(request) => handle(&context, &request),
Err(error) => Some(json_rpc_error(
Value::Null,
-32700,
format!("invalid JSON: {error}"),
)),
};
if let Some(response) = response {
serde_json::to_writer(&mut stdout, &response)?;
stdout.write_all(b"\n")?;
stdout.flush()?;
}
}
// Runs are executed exclusively by the worker daemon; wait-style tools
// only poll the database, so there is nothing to join on shutdown.
Ok(())
}
fn handle(context: &Ctx, request: &Value) -> Option<Value> {
let id = request.get("id")?.clone();
let method = request.get("method").and_then(Value::as_str);
let Some(method) = method else {
return Some(json_rpc_error(id, -32600, "missing method"));
};
let response = match method {
"initialize" => json_rpc_result(
id,
json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {"tools": {"listChanged": false}},
"serverInfo": {"name": "akurai-build", "version": env!("CARGO_PKG_VERSION")},
"instructions": "Full read/write access to AkurAI Build repositories, pipeline runs, artifacts, protected-environment promotions, and Titan workers. Mutating tools (repo_add, repo_host, repo_sync, repo_rename, repo_remove, repo_visibility, run_queue, run_retry, run_promote) change production CI/CD state; repo_remove and run_promote are consequential."
}),
),
"ping" => json_rpc_result(id, json!({})),
"tools/list" => json_rpc_result(id, json!({"tools": tools()})),
"tools/call" => {
let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
let name = params.get("name").and_then(Value::as_str);
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
match name {
Some(name) => match call_tool(context, name, arguments) {
Ok(data) => json_rpc_result(id, tool_result(data, false)),
Err(error) => json_rpc_result(id, tool_result(json!(error.to_string()), true)),
},
None => json_rpc_error(id, -32602, "tool name is required"),
}
}
_ => json_rpc_error(id, -32601, format!("unknown method: {method}")),
};
Some(response)
}
fn call_tool(context: &Ctx, name: &str, arguments: Value) -> Result<Value> {
let database = &context.database;
match name {
"akurai_doctor" => Ok(doctor(context)),
"akurai_repo_list" => {
let arguments: RepositoryArguments = parse(arguments)?;
Ok(to_value(database.query_repositories(
&RepositoryQuery {
search: arguments.search,
visibility: arguments.visibility,
limit: arguments.limit,
offset: arguments.offset,
},
)?)?)
}
"akurai_repo_add" => {
let arguments: RepoAddArguments = parse(arguments)?;
config::validate_repository(&arguments.name, &arguments.url, &arguments.branch)?;
Ok(to_value(database.add_repository(
&arguments.name,
&arguments.url,
&arguments.branch,
)?)?)
}
"akurai_repo_host" => {
let arguments: RepoHostArguments = parse(arguments)?;
let path = hosted_git::host(&context.root, &arguments.name, &arguments.source)?;
match database.add_repository(
&arguments.name,
path.to_str()
.context("hosted repository path is not UTF-8")?,
&arguments.branch,
) {
Ok(repository) => Ok(json!({
"repository": repository,
"clone_url": format!("https://akurai-build.olibuijr.com/git/{}.git", arguments.name)
})),
Err(error) => {
let _ = fs::remove_dir_all(path);
Err(error)
}
}
}
"akurai_repo_sync" => {
let arguments: RepoSyncArguments = parse(arguments)?;
let path = hosted_git::sync(&context.root, &arguments.name, &arguments.source)?;
Ok(json!({"name": arguments.name, "path": path, "synchronized": true}))
}
"akurai_release" => {
let arguments: ReleaseArguments = parse(arguments)?;
// The repository must be registered before a release may target it.
database.repository(&arguments.repository)?;
let outcome = release::release(
&arguments.source,
&arguments.bump,
arguments.notes.as_deref(),
)?;
let mirror = hosted_git::sync(&context.root, &arguments.repository, &arguments.source)?;
let runner = context.runner()?;
let queued =
runner.queue(&arguments.repository, None, Some(&outcome.commit), "manual")?;
let run = if arguments.wait {
wait_for_run(database, queued.id, MAX_BLOCKING_WAIT, 2)?
} else {
json!({"id": queued.id, "status": "queued", "deduplicated": queued.deduplicated})
};
Ok(json!({
"release": to_value(outcome)?,
"mirror": mirror,
"run": run,
}))
}
"akurai_repo_rename" => {
let arguments: RepoRenameArguments = parse(arguments)?;
Ok(to_value(
database.rename_repository(&arguments.old, &arguments.new)?,
)?)
}
"akurai_repo_remove" => {
let arguments: RepoNameArguments = parse(arguments)?;
let repository = database.remove_repository(&arguments.name)?;
let mirror = hosted_git::remove(&context.root, &arguments.name)?;
Ok(json!({
"repository": repository,
"removed": true,
"mirror_deleted": mirror,
}))
}
"akurai_repo_visibility" => {
let arguments: RepoVisibilityArguments = parse(arguments)?;
Ok(to_value(database.set_repository_visibility(
&arguments.repository,
&arguments.visibility,
)?)?)
}
"akurai_repo_branches" => {
let arguments: RepoNameArguments = parse(arguments)?;
Ok(to_value(repo_branches(database, &arguments.name)?)?)
}
"akurai_repo_tree" => {
let arguments: RepoTreeArguments = parse(arguments)?;
let repository = database.repository(&arguments.repository)?;
let sub_path = arguments.path.unwrap_or_default();
Ok(to_value(context.tree.tree(
&repository.name,
&repository.url,
arguments.reference.as_deref(),
&sub_path,
)?)?)
}
"akurai_repo_blob" => {
let arguments: RepoBlobArguments = parse(arguments)?;
let repository = database.repository(&arguments.repository)?;
let reference = arguments
.reference
.as_deref()
.unwrap_or(&repository.default_branch);
Ok(to_value(context.tree.blob(
&repository.name,
&repository.url,
reference,
&arguments.path,
)?)?)
}
"akurai_init" => {
let arguments: InitArguments = parse(arguments)?;
ensure!(
arguments.force || !arguments.out.exists(),
"{} already exists; use force",
arguments.out.display()
);
fs::write(&arguments.out, include_str!("../akurai.example.yml"))?;
Ok(json!({"path": arguments.out, "created": true}))
}
"akurai_run_queue" => {
let arguments: RunQueueArguments = parse(arguments)?;
let runner = context.runner()?;
let queued = runner.queue(
&arguments.repository,
arguments.git_ref.as_deref(),
arguments.commit.as_deref(),
"manual",
)?;
if arguments.wait {
Ok(wait_for_run(database, queued.id, MAX_BLOCKING_WAIT, 2)?)
} else {
let run = database.run(queued.id)?;
Ok(
json!({"id": queued.id, "status": run.status, "deduplicated": queued.deduplicated}),
)
}
}
"akurai_runs" => {
let arguments: RunArguments = parse(arguments)?;
Ok(to_value(
database.query_runs(&RunQuery {
repository: arguments.repo.map(|value| value.values().join(",")),
statuses: arguments.status.map(StringList::values).unwrap_or_default(),
git_ref: arguments.git_ref,
triggers: arguments
.trigger
.map(StringList::values)
.unwrap_or_default(),
search: arguments.search,
before_id: None,
limit: arguments.limit,
offset: arguments.offset,
public_only: false,
})?,
)?)
}
"akurai_run_show" => {
let arguments: RunIdArguments = parse(arguments)?;
Ok(to_value(database.detail(arguments.id)?)?)
}
"akurai_run_cancel" => {
let arguments: RunIdArguments = parse(arguments)?;
database.cancel_run(arguments.id)?;
Ok(to_value(database.detail(arguments.id)?)?)
}
"akurai_run_wait" => {
let arguments: RunWaitArguments = parse(arguments)?;
ensure!(
(1..=14_400).contains(&arguments.timeout),
"timeout must be 1..=14400 seconds"
);
ensure!(
(1..=60).contains(&arguments.interval),
"interval must be 1..=60 seconds"
);
let budget = Duration::from_secs(arguments.timeout).min(MAX_BLOCKING_WAIT);
Ok(wait_for_run(
database,
arguments.id,
budget,
arguments.interval,
)?)
}
"akurai_run_logs" => {
let arguments: RunLogsArguments = parse(arguments)?;
let mut jobs = database.jobs(arguments.id)?;
if arguments.failed {
jobs.retain(|job| {
matches!(job.status.as_str(), "failed" | "interrupted" | "canceled")
});
}
Ok(to_value(
jobs.into_iter()
.map(|job| {
json!({"id": job.id, "name": job.name, "status": job.status, "logs": job.logs})
})
.collect::<Vec<_>>(),
)?)
}
"akurai_run_retry" => {
let arguments: RunRetryArguments = parse(arguments)?;
let id = database.retry(arguments.id)?;
if arguments.wait {
Ok(wait_for_run(database, id, MAX_BLOCKING_WAIT, 2)?)
} else {
Ok(json!({"id": id, "status": "queued"}))
}
}
"akurai_run_promote" => {
let arguments: RunPromoteArguments = parse(arguments)?;
let jobs = database.approve_environment(arguments.id, &arguments.environment)?;
if arguments.wait {
Ok(wait_for_run(database, arguments.id, MAX_BLOCKING_WAIT, 2)?)
} else {
Ok(json!({
"id": arguments.id,
"environment": arguments.environment,
"approved_jobs": jobs,
"status": "queued"
}))
}
}
"akurai_artifact_get" => {
let arguments: ArtifactGetArguments = parse(arguments)?;
ensure!(
arguments.force || !arguments.output.exists(),
"{} exists; use force",
arguments.output.display()
);
let runner = context.runner()?;
let artifact = database.artifact(arguments.id)?;
let source = runner.artifact_path(&artifact)?;
if let Some(parent) = arguments
.output
.parent()
.filter(|path| !path.as_os_str().is_empty())
{
fs::create_dir_all(parent)?;
}
fs::copy(source, &arguments.output)?;
Ok(json!({
"id": arguments.id,
"output": arguments.output,
"sha256": artifact.sha256,
"bytes": artifact.bytes
}))
}
"akurai_delivery_metrics" => {
let repository = arguments.get("repository").and_then(Value::as_str);
let environment = arguments.get("environment").and_then(Value::as_str);
let window = arguments
.get("window_seconds")
.and_then(Value::as_i64)
.unwrap_or(7 * 86_400);
Ok(to_value(database.delivery_metrics(
repository,
environment,
window,
)?)?)
}
"akurai_installation_register" => {
let arguments: InstallationRegisterArguments = parse(arguments)?;
let revision = database.register_installation_revision(
&arguments.installation_id,
&arguments.organization_id,
&arguments.workspace_id,
&arguments.app_id,
&arguments.idempotency_key,
)?;
Ok(json!({
"installation_id": arguments.installation_id,
"revision": revision,
"idempotent": true
}))
}
"akurai_deployments_for_tenant" => {
let arguments: TenantDeploymentsArguments = parse(arguments)?;
Ok(to_value(database.deployments_for_tenant(
arguments.run_id,
&arguments.organization_id,
&arguments.workspace_id,
)?)?)
}
"akurai_workers" => Ok(to_value(database.workers()?)?),
_ => bail!("unknown tool: {name}"),
}
}
/// Poll a run until it reaches a persisted terminal or approval-required
/// state, or the blocking budget elapses. On budget exhaustion the latest
/// run detail is returned (typically still "queued"/"running") instead of an
/// error, so callers always get a response within the MCP client timeout and
/// can follow up with akurai_run_wait / akurai_run_show. The actual pipeline
/// work is performed by the serve daemon's worker loop, never inline here.
fn wait_for_run(
database: &Database,
run_id: i64,
budget: Duration,
interval: u64,
) -> Result<Value> {
let deadline = Instant::now() + budget;
loop {
let detail = database.detail(run_id)?;
let settled = matches!(
detail.run.status.as_str(),
"waiting" | "succeeded" | "failed" | "canceled" | "interrupted"
);
if settled || Instant::now() >= deadline {
return to_value(detail);
}
thread::sleep(Duration::from_secs(interval).min(deadline - Instant::now()));
}
}
fn doctor(context: &Ctx) -> Value {
let git = version("git", &["--version"]);
let docker = version("docker", &["info", "--format", "{{.ServerVersion}}"]);
let database = context
.database
.check_ready()
.map(|()| "ready".to_owned())
.unwrap_or_else(|error| format!("unavailable: {error}"));
json!({"database": database, "git": git, "docker": docker, "data": context.root})
}
fn version(program: &str, arguments: &[&str]) -> Value {
match Command::new(program).args(arguments).output() {
Ok(output) => json!({
"ok": output.status.success(),
"output": String::from_utf8_lossy(if output.status.success() { &output.stdout } else { &output.stderr }).trim()
}),
Err(error) => json!({"ok": false, "output": error.to_string()}),
}
}
fn repo_branches(database: &Database, name: &str) -> Result<Vec<git_process::RemoteBranch>> {
let repository = database.repository(name)?;
git_process::remote_branches(&repository.url, &repository.default_branch)
}
fn parse<T: for<'de> Deserialize<'de>>(arguments: Value) -> Result<T> {
Ok(serde_json::from_value(arguments)?)
}
fn to_value<T: Serialize>(data: T) -> Result<Value> {
Ok(serde_json::to_value(data)?)
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RepositoryArguments {
#[serde(alias = "q")]
search: Option<String>,
visibility: Option<String>,
#[serde(default = "default_repository_limit")]
limit: usize,
#[serde(default)]
offset: usize,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoAddArguments {
name: String,
url: String,
#[serde(default = "default_branch")]
branch: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoHostArguments {
name: String,
source: PathBuf,
#[serde(default = "default_branch")]
branch: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoSyncArguments {
name: String,
source: PathBuf,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ReleaseArguments {
repository: String,
source: PathBuf,
#[serde(default = "default_bump")]
bump: String,
#[serde(default)]
notes: Option<String>,
#[serde(default)]
wait: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoRenameArguments {
old: String,
new: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoNameArguments {
name: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoVisibilityArguments {
repository: String,
visibility: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoTreeArguments {
repository: String,
#[serde(rename = "ref")]
reference: Option<String>,
path: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoBlobArguments {
repository: String,
#[serde(rename = "ref")]
reference: Option<String>,
path: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct InitArguments {
out: PathBuf,
#[serde(default)]
force: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunQueueArguments {
repository: String,
git_ref: Option<String>,
commit: Option<String>,
#[serde(default)]
wait: bool,
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RunArguments {
#[serde(alias = "repository")]
repo: Option<StringList>,
status: Option<StringList>,
git_ref: Option<String>,
trigger: Option<StringList>,
#[serde(alias = "q")]
search: Option<String>,
#[serde(default = "default_run_limit")]
limit: usize,
#[serde(default)]
offset: usize,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunIdArguments {
id: i64,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunWaitArguments {
id: i64,
#[serde(default = "default_wait_timeout")]
timeout: u64,
#[serde(default = "default_wait_interval")]
interval: u64,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunLogsArguments {
id: i64,
#[serde(default)]
failed: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunRetryArguments {
id: i64,
#[serde(default)]
wait: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RunPromoteArguments {
id: i64,
environment: String,
#[serde(default)]
wait: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ArtifactGetArguments {
id: i64,
output: PathBuf,
#[serde(default)]
force: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct InstallationRegisterArguments {
installation_id: String,
organization_id: String,
workspace_id: String,
app_id: String,
idempotency_key: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TenantDeploymentsArguments {
run_id: i64,
organization_id: String,
workspace_id: String,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum StringList {
One(String),
Many(Vec<String>),
}
impl StringList {
fn values(self) -> Vec<String> {
let values = match self {
Self::One(value) => vec![value],
Self::Many(values) => values,
};
values
.into_iter()
.flat_map(|value| {
value
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>()
})
.collect()
}
}
const fn default_repository_limit() -> usize {
50
}
const fn default_run_limit() -> usize {
20
}
const fn default_wait_timeout() -> u64 {
3600
}
const fn default_wait_interval() -> u64 {
2
}
fn default_branch() -> String {
"main".to_owned()
}
fn default_bump() -> String {
"patch".to_owned()
}
fn tools() -> Vec<Value> {
vec![
json!({
"name": "akurai_doctor",
"description": "Run controller diagnostics (database readiness, git, docker) without changing state.",
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": false}
}),
json!({
"name": "akurai_repo_list",
"description": "Query registered Git repositories by text and visibility.",
"inputSchema": {
"type": "object",
"properties": {
"search": {"type": "string", "description": "LIKE search across name, URL, and default branch."},
"visibility": {"type": "string", "enum": ["private", "public"]},
"limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
"offset": {"type": "integer", "minimum": 0, "maximum": 10000, "default": 0}
},
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_add",
"description": "Register an external Git repository by URL for CI.",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"url": {"type": "string"},
"branch": {"type": "string", "default": "main"}
},
"required": ["name", "url"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_host",
"description": "Create a bare mirror owned by AkurAI Build from a trusted Titan checkout and register it for CI.",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"source": {"type": "string", "description": "Absolute path to the trusted Titan checkout."},
"branch": {"type": "string", "default": "main"}
},
"required": ["name", "source"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_sync",
"description": "Synchronize an existing AkurAI Build mirror from its trusted Titan checkout.",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"source": {"type": "string", "description": "Absolute path to the trusted Titan checkout."}
},
"required": ["name", "source"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_release",
"description": "Release a product: bump the version, cut the changelog, commit + tag in the trusted checkout, sync the mirror, and queue a pipeline run for the release commit. The single maintained release path for every product.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string", "description": "Registered repository name."},
"source": {"type": "string", "description": "Absolute path to the trusted Titan checkout."},
"bump": {"type": "string", "enum": ["patch", "minor", "major"], "default": "patch"},
"notes": {"type": "string", "description": "Changelog body for the cut section; defaults to the changelog's Unreleased content."},
"wait": {"type": "boolean", "default": false, "description": "Block until the queued run reaches a terminal or approval-required state."}
},
"required": ["repository", "source"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_rename",
"description": "Rename a registered repository.",
"inputSchema": {
"type": "object",
"properties": {"old": {"type": "string"}, "new": {"type": "string"}},
"required": ["old", "new"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_remove",
"description": "Unregister a repository and delete its hosted mirror. Cascades runs and artifacts. Destructive.",
"inputSchema": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_visibility",
"description": "Set a repository's dashboard visibility.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string"},
"visibility": {"type": "string", "enum": ["private", "public"]}
},
"required": ["repository", "visibility"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_branches",
"description": "List remote branch heads for a registered repository via git ls-remote.",
"inputSchema": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_tree",
"description": "Browse a repository's file tree at a Git ref and sub-path.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string"},
"ref": {"type": "string", "description": "Git ref; defaults to the repository's HEAD."},
"path": {"type": "string", "description": "Sub-path within the tree; defaults to the root."}
},
"required": ["repository"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_repo_blob",
"description": "Read a bounded UTF-8 file from a repository at a Git ref and path.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string"},
"ref": {"type": "string", "description": "Git ref; defaults to the repository's default branch."},
"path": {"type": "string", "description": "File path within the repository."}
},
"required": ["repository", "path"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_init",
"description": "Write a minimal .akurai.yml pipeline template to a path on the Titan filesystem.",
"inputSchema": {
"type": "object",
"properties": {
"out": {"type": "string"},
"force": {"type": "boolean", "default": false}
},
"required": ["out"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_queue",
"description": "Queue a pipeline run for a registered repository, optionally blocking until it reaches a terminal or approval-required state.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string"},
"git_ref": {"type": "string"},
"commit": {"type": "string", "description": "Full hexadecimal commit SHA to pin."},
"wait": {"type": "boolean", "default": false}
},
"required": ["repository"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_runs",
"description": "Query runs across one, many, or all repositories with composable filters.",
"inputSchema": {
"type": "object",
"properties": {
"repo": string_or_array("Repository name, comma-separated names, or an array."),
"repository": string_or_array("Repository name, comma-separated names, or an array."),
"status": string_or_array("Status or statuses: queued, running, waiting, succeeded, failed, canceled, interrupted."),
"git_ref": {"type": "string", "description": "Exact Git ref."},
"trigger": string_or_array("Trigger or triggers: manual, webhook, retry."),
"search": {"type": "string", "description": "LIKE search across repository, ref, commit SHA, and retained error."},
"limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 20},
"offset": {"type": "integer", "minimum": 0, "maximum": 10000, "default": 0}
},
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_show",
"description": "Read one run with jobs, bounded logs, artifacts, and deployments.",
"inputSchema": {
"type": "object",
"properties": {"id": {"type": "integer", "minimum": 1}},
"required": ["id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_cancel",
"description": "Cancel a queued or waiting run (and its unstarted jobs). Running and terminal runs are refused; the worker owns a running run.",
"inputSchema": {
"type": "object",
"properties": {"id": {"type": "integer", "minimum": 1}},
"required": ["id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_wait",
"description": "Poll a run until it reaches a persisted terminal or approval-required state, or the 25s blocking budget elapses (then returns the latest run state; call again to keep waiting).",
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"timeout": {"type": "integer", "minimum": 1, "maximum": 14400, "default": 3600},
"interval": {"type": "integer", "minimum": 1, "maximum": 60, "default": 2}
},
"required": ["id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_logs",
"description": "Return job logs for a run, optionally filtered to failed/canceled/interrupted jobs.",
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"failed": {"type": "boolean", "default": false}
},
"required": ["id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_retry",
"description": "Queue the same immutable revision as a prior run, optionally waiting.",
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"wait": {"type": "boolean", "default": false}
},
"required": ["id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_run_promote",
"description": "Approve and resume a protected environment on a run; the worker daemon executes it. With wait, polls up to 25s and returns the latest run state. Production-impacting.",
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"environment": {"type": "string"},
"wait": {"type": "boolean", "default": false}
},
"required": ["id", "environment"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_artifact_get",
"description": "Copy a build artifact to a path on the Titan filesystem.",
"inputSchema": {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"output": {"type": "string"},
"force": {"type": "boolean", "default": false}
},
"required": ["id", "output"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_delivery_metrics",
"description": "Read source-derived DORA delivery metrics with freshness, sample size, and immutable run/deployment drill-down IDs.",
"inputSchema": {
"type": "object",
"properties": {
"repository": {"type": "string"},
"environment": {"type": "string"},
"window_seconds": {"type": "integer", "minimum": 1, "maximum": 2592000, "default": 604800}
},
"additionalProperties": false
}
}),
json!({
"name": "akurai_installation_register",
"description": "Register an installation revision from trusted provisioning, idempotently.",
"inputSchema": {
"type": "object",
"properties": {
"installation_id": {"type": "string"},
"organization_id": {"type": "string"},
"workspace_id": {"type": "string"},
"app_id": {"type": "string"},
"idempotency_key": {"type": "string"}
},
"required": ["installation_id", "organization_id", "workspace_id", "app_id", "idempotency_key"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_deployments_for_tenant",
"description": "Read managed deployments for a run within an authorized organization and workspace tenant.",
"inputSchema": {
"type": "object",
"properties": {
"run_id": {"type": "integer", "minimum": 1},
"organization_id": {"type": "string"},
"workspace_id": {"type": "string"}
},
"required": ["run_id", "organization_id", "workspace_id"],
"additionalProperties": false
}
}),
json!({
"name": "akurai_workers",
"description": "List Titan worker status, capacity, current run, heartbeat, and completed-run count.",
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": false}
}),
]
}
fn string_or_array(description: &str) -> Value {
json!({
"description": description,
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}, "maxItems": 32}
]
})
}
fn tool_result(data: Value, is_error: bool) -> Value {
let text = if is_error {
data.as_str().unwrap_or("tool failed").to_owned()
} else {
serde_json::to_string_pretty(&data).unwrap_or_else(|_| "null".into())
};
json!({"content": [{"type": "text", "text": text}], "isError": is_error})
}
fn json_rpc_result(id: Value, result: Value) -> Value {
json!({"jsonrpc": "2.0", "id": id, "result": result})
}
fn json_rpc_error(id: Value, code: i64, message: impl Into<String>) -> Value {
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message.into()}})
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
fn context() -> Ctx {
let database = Database::memory(KEY).expect("open in-memory database");
database.migrate().expect("migrate schema");
let root = std::env::temp_dir().join(format!("akurai-mcp-test-{}", std::process::id()));
Ctx {
database,
root: root.clone(),
allow_native: false,
tree: RepoTree::new(&root).expect("create tree root"),
}
}
#[test]
fn lists_tools() {
let tools = tools();
let names: Vec<&str> = tools
.iter()
.map(|tool| tool["name"].as_str().expect("tool name"))
.collect();
assert!(names.contains(&"akurai_repo_add"));
assert!(names.contains(&"akurai_run_queue"));
assert!(names.contains(&"akurai_run_promote"));
assert!(names.contains(&"akurai_run_cancel"));
assert!(names.contains(&"akurai_repo_visibility"));
assert!(names.contains(&"akurai_repo_branches"));
assert!(names.contains(&"akurai_repo_tree"));
assert!(names.contains(&"akurai_repo_blob"));
}
#[test]
fn installation_registration_tool_is_idempotent() -> Result<()> {
let context = context();
let first = call_tool(
&context,
"akurai_installation_register",
json!({
"installation_id": "inst-1",
"organization_id": "org-1",
"workspace_id": "ws-1",
"app_id": "app-1",
"idempotency_key": "request-1"
}),
)?;
let second = call_tool(
&context,
"akurai_installation_register",
json!({
"installation_id": "inst-1",
"organization_id": "org-1",
"workspace_id": "ws-1",
"app_id": "app-1",
"idempotency_key": "request-1"
}),
)?;
assert_eq!(first["revision"], 1);
assert_eq!(second["revision"], 1);
Ok(())
}
#[test]
fn repo_lifecycle_through_tools() {
let context = context();
let added = call_tool(
&context,
"akurai_repo_add",
json!({"name": "demo", "url": "https://example.com/demo.git", "branch": "main"}),
)
.expect("add repository");
assert_eq!(added["name"], "demo");
let listed = call_tool(&context, "akurai_repo_list", json!({"search": "demo"}))
.expect("list repositories");
assert_eq!(listed.as_array().expect("array").len(), 1);
let renamed = call_tool(
&context,
"akurai_repo_rename",
json!({"old": "demo", "new": "demo2"}),
)
.expect("rename repository");
assert_eq!(renamed["name"], "demo2");
let visibility = call_tool(
&context,
"akurai_repo_visibility",
json!({"repository": "demo2", "visibility": "public"}),
)
.expect("set visibility");
assert_eq!(visibility["visibility"], "public");
let removed = call_tool(&context, "akurai_repo_remove", json!({"name": "demo2"}))
.expect("remove repository");
assert_eq!(removed["removed"], true);
}
#[test]
fn unknown_tool_errors() {
let context = context();
let error = call_tool(&context, "akurai_nope", json!({})).expect_err("should fail");
assert!(error.to_string().contains("unknown tool"));
}
#[test]
fn initialize_and_tools_list_round_trip() {
let context = context();
let init = handle(
&context,
&json!({"jsonrpc":"2.0","id":1,"method":"initialize"}),
)
.expect("initialize response");
assert_eq!(init["result"]["serverInfo"]["name"], "akurai-build");
let list = handle(
&context,
&json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}),
)
.expect("tools/list response");
assert!(
!list["result"]["tools"]
.as_array()
.expect("array")
.is_empty()
);
}
#[test]
fn repository_blob_tool_reads_file_at_commit() -> Result<()> {
let temporary = tempfile::tempdir()?;
let source = temporary.path().join("source");
fs::create_dir(&source)?;
let git = |arguments: &[&str]| -> Result<()> {
let output = Command::new("git")
.current_dir(&source)
.args(arguments)
.output()?;
ensure!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
};
git(&["init", "-b", "main"])?;
fs::write(source.join("fixture.txt"), "fixture")?;
git(&["add", "fixture.txt"])?;
git(&[
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"-m",
"init",
])?;
let output = Command::new("git")
.current_dir(&source)
.args(["rev-parse", "HEAD"])
.output()?;
ensure!(output.status.success(), "git rev-parse failed");
let sha = String::from_utf8(output.stdout)?.trim().to_owned();
let database = Database::memory(KEY)?;
database.migrate()?;
database.add_repository(
"demo",
source.to_str().context("UTF-8 source path")?,
"main",
)?;
let root = temporary.path().join("data");
let context = Ctx {
database,
root: root.clone(),
allow_native: false,
tree: RepoTree::new(&root)?,
};
let blob = call_tool(
&context,
"akurai_repo_blob",
json!({"repository": "demo", "ref": sha, "path": "fixture.txt"}),
)?;
assert_eq!(blob["content"], "fixture");
assert_eq!(blob["path"], "fixture.txt");
Ok(())
}
}