use std::io::{BufRead, Write};
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use axiom_engine::agent_task::{AgentTask, FileEdit, TaskRegistry};
use axiom_engine::verifier_strength::check_verifier_strength;
const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Deserialize)]
struct TaskStartParams {
goal: String,
verify_cmd: String,
#[serde(default)]
files: Vec<String>,
#[serde(default = "default_max_attempts")]
max_attempts: usize,
#[serde(default)]
min_mutation_score: Option<f64>,
}
fn default_max_attempts() -> usize {
4
}
#[derive(Deserialize)]
struct TaskProposeParams {
task_id: String,
edits: Vec<EditParam>,
}
#[derive(Deserialize)]
struct EditParam {
path: String,
content: String,
}
#[derive(Deserialize)]
struct TaskHistoryParams {
task_id: String,
}
#[derive(Deserialize)]
struct TaskFinishParams {
task_id: String,
#[serde(default = "default_commit")]
commit: bool,
}
fn default_commit() -> bool {
true
}
#[derive(Serialize)]
struct RpcError {
code: i32,
message: String,
}
fn ok(id: &Value, result: Value) -> String {
serde_json::to_string(&json!({"jsonrpc":"2.0","id":id,"result":result})).unwrap()
}
fn err(id: &Value, code: i32, message: String) -> String {
let e = RpcError { code, message };
serde_json::to_string(&json!({"jsonrpc":"2.0","id":id,"error":e})).unwrap()
}
fn new_task_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static SEQ: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
format!("task-{nanos:x}-{seq}")
}
fn main() {
let registry = TaskRegistry::new();
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let line = line.trim();
if line.is_empty() {
continue;
}
let msg: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
let _ = writeln!(stdout, "{}", err(&Value::Null, -32700, format!("parse error: {e}")));
let _ = stdout.flush();
continue;
}
};
let has_id = msg.get("id").is_some();
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
let params = msg.get("params").cloned().unwrap_or(Value::Null);
let response = match method {
"initialize" => handle_initialize(&id, params),
"tools/list" => handle_tools_list(&id),
"tools/call" => handle_tools_call(®istry, &id, params),
"notifications/initialized" => String::new(),
"task_start" => handle_start(®istry, &id, params),
"task_propose" => handle_propose(®istry, &id, params),
"task_history" => handle_history(®istry, &id, params),
"task_finish" => handle_finish(®istry, &id, params),
_ => err(&id, -32601, format!("unknown method: {method}")),
};
if has_id {
let _ = writeln!(stdout, "{response}");
let _ = stdout.flush();
}
}
}
fn mutation_gate_result(task: &AgentTask) -> Option<Value> {
let threshold = task.min_mutation_score?;
Some(match &task.mutation_score {
Some(score) => json!({
"status": "measured",
"threshold": threshold,
"score": score.score,
"tool": score.tool,
}),
None => json!({
"status": "skipped",
"threshold": threshold,
}),
})
}
fn handle_start(registry: &TaskRegistry, id: &Value, params: Value) -> String {
let p: TaskStartParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return err(id, -32602, format!("invalid params: {e}")),
};
let task_id = new_task_id();
let files: Vec<PathBuf> = p.files.into_iter().map(PathBuf::from).collect();
let verifier_warning = check_verifier_strength(&p.verify_cmd);
match AgentTask::start(
task_id.clone(),
p.goal,
p.verify_cmd,
files,
p.max_attempts,
p.min_mutation_score,
) {
Ok(task) => {
let mut result = json!({"task_id": task_id});
if let Some(warning) = verifier_warning {
result["verifier_warning"] = json!(warning);
}
if let Some(gate) = mutation_gate_result(&task) {
result["mutation_gate"] = gate;
}
registry.insert(task);
ok(id, result)
}
Err(e) => {
let code = if e.kind() == std::io::ErrorKind::InvalidInput {
-32602
} else {
-32000
};
err(id, code, format!("task_start failed: {e}"))
}
}
}
fn handle_propose(registry: &TaskRegistry, id: &Value, params: Value) -> String {
let p: TaskProposeParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return err(id, -32602, format!("invalid params: {e}")),
};
let edits: Vec<FileEdit> = p
.edits
.into_iter()
.map(|e| FileEdit {
path: PathBuf::from(e.path),
content: e.content,
})
.collect();
match registry.with_task(&p.task_id, |t| t.propose(edits)) {
Some(outcome) => ok(
id,
json!({
"passed": outcome.passed,
"attempt": outcome.attempt,
"output": outcome.output,
"fingerprint": outcome.fingerprint,
}),
),
None => err(id, -32001, format!("unknown task_id: {}", p.task_id)),
}
}
fn handle_history(registry: &TaskRegistry, id: &Value, params: Value) -> String {
let p: TaskHistoryParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return err(id, -32602, format!("invalid params: {e}")),
};
match registry.with_task(&p.task_id, |t| t.history().to_vec()) {
Some(history) => ok(id, json!({"attempts": history})),
None => err(id, -32001, format!("unknown task_id: {}", p.task_id)),
}
}
fn handle_finish(registry: &TaskRegistry, id: &Value, params: Value) -> String {
let p: TaskFinishParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return err(id, -32602, format!("invalid params: {e}")),
};
let result = registry.with_task(&p.task_id, |task| task.finish(p.commit));
match result {
Some(Ok(())) => {
registry.remove(&p.task_id);
ok(id, json!({"committed": p.commit}))
}
Some(Err(e)) => err(id, -32002, format!("finish failed, task retained for retry: {e}")),
None => err(id, -32001, format!("unknown task_id: {}", p.task_id)),
}
}
fn handle_initialize(id: &Value, _params: Value) -> String {
ok(
id,
json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"capabilities": {"tools": {}},
"serverInfo": {"name": "axiom-agent-task", "version": SERVER_VERSION},
}),
)
}
fn tool_def(name: &str, description: &str, schema: Value) -> Value {
json!({"name": name, "description": description, "inputSchema": schema})
}
fn handle_tools_list(id: &Value) -> String {
let tools = vec![
tool_def(
"task_start",
"Start a verifier-gated agent task. Snapshots the listed files and returns a task_id.",
json!({
"type": "object",
"properties": {
"goal": {"type": "string", "description": "What the task should achieve."},
"verify_cmd": {"type": "string", "description": "Shell command run to verify each proposal; exit 0 means pass."},
"files": {"type": "array", "items": {"type": "string"}, "description": "File paths the task may modify."},
"max_attempts": {"type": "integer", "description": "Maximum distinct proposals before the task refuses more.", "default": 4},
},
"required": ["goal", "verify_cmd"],
}),
),
tool_def(
"task_propose",
"Propose an edit-set for a task. The edits are applied transactionally and the verifier runs; failures roll back byte-for-byte.",
json!({
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Task ID from task_start."},
"edits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["path", "content"],
},
"description": "Edits to apply atomically.",
},
},
"required": ["task_id", "edits"],
}),
),
tool_def(
"task_history",
"Return all recorded attempts for a task in order.",
json!({
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Task ID from task_start."},
},
"required": ["task_id"],
}),
),
tool_def(
"task_finish",
"Finish a task. commit=true keeps the last passing edits; commit=false restores the pre-task snapshot.",
json!({
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Task ID from task_start."},
"commit": {"type": "boolean", "description": "Keep edits (true) or restore snapshot (false).", "default": true},
},
"required": ["task_id"],
}),
),
];
ok(id, json!({"tools": tools}))
}
#[derive(Deserialize)]
struct ToolsCallParams {
name: String,
#[serde(default)]
arguments: Value,
}
fn handle_tools_call(registry: &TaskRegistry, id: &Value, params: Value) -> String {
let p: ToolsCallParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return err(id, -32602, format!("invalid params: {e}")),
};
let arguments = if p.arguments.is_null() {
json!({})
} else {
p.arguments
};
let inner: Value = match p.name.as_str() {
"task_start" => match serde_json::from_value::<TaskStartParams>(arguments) {
Ok(sp) => {
let task_id = new_task_id();
let files: Vec<PathBuf> = sp.files.into_iter().map(PathBuf::from).collect();
match AgentTask::start(
task_id.clone(),
sp.goal,
sp.verify_cmd,
files,
sp.max_attempts,
sp.min_mutation_score,
) {
Ok(task) => {
let mut result = json!({"task_id": task_id});
if let Some(gate) = mutation_gate_result(&task) {
result["mutation_gate"] = gate;
}
registry.insert(task);
result
}
Err(e) => return tool_error(id, format!("task_start failed: {e}")),
}
}
Err(e) => return tool_error(id, format!("invalid arguments: {e}")),
},
"task_propose" => match serde_json::from_value::<TaskProposeParams>(arguments) {
Ok(pp) => {
let edits: Vec<FileEdit> = pp
.edits
.into_iter()
.map(|e| FileEdit {
path: PathBuf::from(e.path),
content: e.content,
})
.collect();
match registry.with_task(&pp.task_id, |t| t.propose(edits)) {
Some(outcome) => json!({
"passed": outcome.passed,
"attempt": outcome.attempt,
"output": outcome.output,
"fingerprint": outcome.fingerprint,
}),
None => return tool_error(id, format!("unknown task_id: {}", pp.task_id)),
}
}
Err(e) => return tool_error(id, format!("invalid arguments: {e}")),
},
"task_history" => match serde_json::from_value::<TaskHistoryParams>(arguments) {
Ok(hp) => match registry.with_task(&hp.task_id, |t| t.history().to_vec()) {
Some(history) => json!({"attempts": history}),
None => return tool_error(id, format!("unknown task_id: {}", hp.task_id)),
},
Err(e) => return tool_error(id, format!("invalid arguments: {e}")),
},
"task_finish" => match serde_json::from_value::<TaskFinishParams>(arguments) {
Ok(fp) => {
let result = registry.with_task(&fp.task_id, |task| task.finish(fp.commit));
match result {
Some(Ok(())) => {
registry.remove(&fp.task_id);
json!({"committed": fp.commit})
}
Some(Err(e)) => {
return tool_error(
id,
format!("finish failed, task retained for retry: {e}"),
)
}
None => return tool_error(id, format!("unknown task_id: {}", fp.task_id)),
}
}
Err(e) => return tool_error(id, format!("invalid arguments: {e}")),
},
other => return err(id, -32602, format!("unknown tool: {other}")),
};
ok(
id,
json!({"content": [{"type": "text", "text": inner.to_string()}]}),
)
}
fn tool_error(id: &Value, message: String) -> String {
ok(
id,
json!({
"content": [{"type": "text", "text": message}],
"isError": true,
}),
)
}