use clap::Parser;
use serde_json::{Value, json};
use std::collections::BTreeSet;
use std::io::{BufRead, BufReader, Read as _, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::{Arc, Mutex};
#[derive(Parser)]
struct Args {
#[arg(long)]
control: PathBuf,
#[arg(long, default_value_t = 1)]
version: u16,
#[arg(long)]
no_load: bool,
#[arg(long)]
hold: bool,
#[arg(long, default_value_t = 0)]
burst: usize,
#[arg(long)]
exit: Option<u8>,
#[arg(long, default_value_t = 262_144)]
frame_bytes: usize,
}
struct State {
prompt: Option<Value>,
turn: u64,
answered: BTreeSet<ValueKey>,
stdout: std::io::Stdout,
control: UnixStream,
version: u16,
bound: usize,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ValueKey(String);
impl State {
fn wire(&mut self, value: &Value) -> anyhow::Result<()> {
line(&mut self.stdout, value, self.bound)
}
fn signal(&mut self, value: &Value) -> anyhow::Result<()> {
line(&mut self.control, value, self.bound)
}
fn update(&mut self, update: &Value) -> anyhow::Result<()> {
self.wire(&json!({"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"fake-session","update":update}}))
}
fn finish(&mut self, reason: &str) -> anyhow::Result<()> {
if let Some(id) = self.prompt.take() {
if self.version == 2 {
self.update(
&json!({"sessionUpdate":"state_update","state":"idle","stopReason":reason}),
)?;
} else {
self.wire(&json!({"jsonrpc":"2.0","id":id,"result":{"stopReason":reason}}))?;
}
self.signal(&json!({"event":"turn_ended","turn":self.turn}))?;
}
Ok(())
}
}
const OFFERED_MODELS: [&str; 2] = ["fake-small", "fake-large"];
fn options(current: &Value) -> Value {
json!([{"id":"model","name":"Model","type":"select","currentValue":current,
"options":OFFERED_MODELS.iter().map(|model| json!({"value":model,"name":model})).collect::<Vec<_>>()}])
}
fn line<W: Write>(writer: &mut W, value: &Value, bound: usize) -> anyhow::Result<()> {
let mut bytes = serde_json::to_vec(value)?;
bytes.push(b'\n');
anyhow::ensure!(bytes.len() <= bound, "fake output frame bound exceeded");
writer.write_all(&bytes)?;
writer.flush()?;
Ok(())
}
fn read<R: BufRead>(reader: &mut R, bound: usize) -> anyhow::Result<Option<Value>> {
let mut bytes = Vec::new();
let count = reader
.take(u64::try_from(bound)?.saturating_add(1))
.read_until(b'\n', &mut bytes)?;
if count == 0 {
return Ok(None);
}
anyhow::ensure!(
count <= bound && bytes.last() == Some(&b'\n'),
"fake input frame bound or newline refused"
);
Ok(Some(
serde_json::from_slice(&bytes).map_err(|_| anyhow::anyhow!("fake input is not JSON"))?,
))
}
fn run(args: &Args) -> anyhow::Result<u8> {
anyhow::ensure!(
args.frame_bytes > 0 && args.burst <= args.frame_bytes,
"fake bounds refused"
);
let control = UnixStream::connect(&args.control)?;
let control_reader = control.try_clone()?;
let state = Arc::new(Mutex::new(State {
prompt: None,
turn: 0,
answered: BTreeSet::new(),
stdout: std::io::stdout(),
control,
version: args.version,
bound: args.frame_bytes,
}));
let controls = Arc::clone(&state);
let bound = args.frame_bytes;
std::thread::spawn(move || {
let mut reader = BufReader::new(control_reader);
let result: anyhow::Result<()> = (|| {
while let Some(command) = read(&mut reader, bound)? {
let mut state = controls
.lock()
.map_err(|_| anyhow::anyhow!("fake control lock poisoned"))?;
match command["command"].as_str() {
Some("finish") => state.finish("end_turn")?,
Some("exit") => {
state.signal(&json!({"event":"exit_requested"}))?;
std::process::exit(3);
}
_ => anyhow::bail!("fake control command refused"),
}
drop(state);
}
Ok(())
})();
if result.is_err() {
eprintln!("fake ACP control failed");
}
});
let stdin = std::io::stdin();
let mut reader = stdin.lock();
while let Some(message) = read(&mut reader, bound)? {
let mut state = state
.lock()
.map_err(|_| anyhow::anyhow!("fake state lock poisoned"))?;
match message["method"].as_str() {
Some("initialize") => {
anyhow::ensure!(
message["params"]["protocolVersion"] == 2,
"host did not offer ACP 2"
);
state.signal(&json!({"event":"initialize","offered":2}))?;
let result = if args.version == 2 {
json!({"protocolVersion":2,"info":{"name":"fake","version":"1"},"capabilities":{"session":{}}})
} else {
json!({"protocolVersion":args.version,"agentCapabilities":{"loadSession":!args.no_load},"authMethods":[]})
};
state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":result}))?;
}
Some(method @ ("session/new" | "session/load" | "session/resume")) => {
anyhow::ensure!(
message["params"]["mcpServers"][0]["name"] == "liminal",
"fake received no ticket"
);
state.signal(&json!({"event":"session_opened","method":method}))?;
let result = if method == "session/new" {
json!({"sessionId":"fake-session","configOptions":options(&json!(OFFERED_MODELS[0]))})
} else {
json!({})
};
state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":result}))?;
}
Some("session/list") => listed(&mut state, &message)?,
Some("session/set_config_option") => configured(&mut state, &message)?,
Some("session/prompt") => {
if let Some(code) = prompt(&mut state, args, &message)? {
return Ok(code);
}
}
Some("session/cancel") => {
state.signal(&json!({"event":"cancel"}))?;
state.finish("cancelled")?;
}
Some(_) => anyhow::bail!("fake received an unknown method"),
None => {
let id = ValueKey(serde_json::to_string(&message["id"])?);
anyhow::ensure!(
state.answered.insert(id),
"fake received a duplicate permission answer"
);
let answered = state.answered.len();
state.signal(&json!({"event":"permission_answer","count":answered,"outcome":message["result"]["outcome"]}))?;
if answered == args.burst {
state.finish("end_turn")?;
}
}
}
}
Ok(0)
}
fn listed(state: &mut State, message: &Value) -> anyhow::Result<()> {
state.signal(&json!({"event":"session_list"}))?;
let sessions = json!([
{"sessionId":"fake-earlier","title":"Earlier conversation","updatedAt":"2026-10-15T09:00:00Z"},
{"sessionId":"fake-session","title":"This conversation","updatedAt":"2026-10-16T09:00:00Z"},
]);
state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":{"sessions":sessions}}))?;
Ok(())
}
fn configured(state: &mut State, message: &Value) -> anyhow::Result<()> {
let value = &message["params"]["value"];
state.signal(
&json!({"event":"config_set","configId":message["params"]["configId"],"value":value}),
)?;
let answer = if message["params"]["configId"] == "model"
&& OFFERED_MODELS.iter().any(|model| value == model)
{
json!({"jsonrpc":"2.0","id":message["id"],"result":{"configOptions":options(value)}})
} else {
json!({"jsonrpc":"2.0","id":message["id"],"error":{"code":-32602,"message":"fake offers no such value"}})
};
state.wire(&answer)?;
Ok(())
}
fn prompt(state: &mut State, args: &Args, message: &Value) -> anyhow::Result<Option<u8>> {
anyhow::ensure!(state.prompt.is_none(), "host sent two simultaneous prompts");
anyhow::ensure!(
message["params"]["sessionId"] == "fake-session",
"prompt names a foreign session"
);
state.turn += 1;
state.prompt = Some(message["id"].clone());
let turn = state.turn;
state.signal(
&json!({"event":"prompt","turn":turn,"text":message["params"]["prompt"][0]["text"]}),
)?;
if args.version == 2 {
state.update(&json!({"sessionUpdate":"state_update","state":"running"}))?;
state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":{}}))?;
}
let tool = if args.version == 2 {
"tool_call_update"
} else {
"tool_call"
};
state.update(&json!({"sessionUpdate":tool,"toolCallId":"tool","title":"Synthetic review","status":"in_progress","rawInput":{"private":"input"},"rawOutput":{"private":"output"},"content":[{"type":"content","content":{"type":"text","text":"file contents"}}]}))?;
state.update(&json!({"sessionUpdate":"agent_message_chunk","messageId":format!("message-{}", state.turn),"content":{"type":"text","text":"Synthetic answer"}}))?;
if let Some(code) = args.exit {
return Ok(Some(code));
}
for id in 0..args.burst {
let options = json!([{"optionId":"yes","name":"Approve","kind":"allow_once"},{"optionId":"no","name":"Refuse","kind":"reject_once"}]);
let params = if args.version == 2 {
json!({"sessionId":"fake-session","title":"Synthetic review","options":options})
} else {
json!({"sessionId":"fake-session","toolCall":{"toolCallId":format!("tool-{id}"),"title":"Synthetic review"},"options":options})
};
state.wire(&json!({"jsonrpc":"2.0","id":format!("permission-{id}"),"method":"session/request_permission","params":params}))?;
}
if args.burst == 0 && !args.hold {
state.finish("end_turn")?;
}
Ok(None)
}
fn main() -> ExitCode {
run(&Args::parse()).map_or_else(
|_| {
eprintln!("fake ACP protocol failed");
ExitCode::from(1)
},
ExitCode::from,
)
}