use crate::automation::command::{Envelope as AppEnvelope, Reply as AppReply};
use crate::automation::queue::AutomationQueue;
use brep_mcp_core::host::{Backend, Envelope, HostInfo, HostRequest, Reply};
use brep_mcp_core::server::BrepServer;
use serde_json::{json, Value};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
pub const DEFAULT_PORT: u16 = 8765;
const REPLY_TIMEOUT: Duration = Duration::from_secs(120);
pub struct Launch {
pub listener: TcpListener,
pub session_root: PathBuf,
}
pub fn bind(port: u16) -> Result<TcpListener, String> {
TcpListener::bind(("127.0.0.1", port)).map_err(|e| format!("cannot listen on 127.0.0.1:{port}: {e}"))
}
pub fn url(listener: &TcpListener) -> String {
let port = listener.local_addr().map(|a| a.port()).unwrap_or(DEFAULT_PORT);
brep_mcp_core::http::url(port)
}
pub fn default_session_root() -> PathBuf {
std::env::var_os("BREP_MCP_SESSION_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir().join("brep-mcp"))
}
pub fn agent_instructions(url: &str, session_root: &Path) -> String {
format!(
"BREP MCP server listening on {url}\n\
This window is the session: an agent's tools act on the documents you see here.\n\
\n\
Claude Code:\n\
\x20 claude mcp add --transport http brep {url}\n\
\x20 or in a project's .mcp.json:\n\
\x20 {{ \"mcpServers\": {{ \"brep\": {{ \"type\": \"http\", \"url\": \"{url}\" }} }} }}\n\
\n\
Codex:\n\
\x20 codex mcp add brep --url {url}\n\
\x20 or in ~/.codex/config.toml:\n\
\x20 [mcp_servers.brep]\n\
\x20 url = \"{url}\"\n\
\n\
Then ask the agent to call session_start; screenshots and call logs land under\n\
\x20 {root}\n",
root = session_root.display(),
)
}
fn convert(mut r: AppReply) -> Reply {
let blob = r.blob.take();
let (id, frame) = (r.id, r.frame);
match serde_json::to_value(&r).map_err(|e| e.to_string()).and_then(|v| Reply::from_app(v, blob)) {
Ok(reply) => reply,
Err(e) => Reply::err(id, frame, e),
}
}
async fn forward(queue: &Arc<AutomationQueue>, envelope: Envelope) -> Reply {
let id = envelope.id;
let rx = queue.submit(AppEnvelope { id, cmd: envelope.cmd, args: envelope.args });
match tokio::task::spawn_blocking(move || rx.recv_timeout(REPLY_TIMEOUT)).await {
Ok(Ok(reply)) => convert(reply),
Ok(Err(std::sync::mpsc::RecvTimeoutError::Timeout)) => Reply::err(
id,
queue.frame(),
format!("no reply from the app within {} s (is the window responding?)", REPLY_TIMEOUT.as_secs()),
),
Ok(Err(std::sync::mpsc::RecvTimeoutError::Disconnected)) => Reply::err(id, queue.frame(), "the app dropped the command"),
Err(e) => Reply::err(id, queue.frame(), e.to_string()),
}
}
async fn capture(queue: &Arc<AutomationQueue>, region: Value) -> Result<(Value, Vec<u8>), String> {
let reply = forward(queue, Envelope { id: 0, cmd: "screenshot".into(), args: json!({ "region": region }) }).await;
if !reply.ok {
return Err(reply.error.unwrap_or_else(|| "screenshot failed".into()));
}
let png = reply.blob.ok_or("the screenshot reply carried no image")?;
Ok((reply.result.unwrap_or(Value::Null), png))
}
pub fn start(queue: Arc<AutomationQueue>, adapter: String, launch: Launch) -> Result<(), String> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_name("brep-mcp-worker")
.enable_all()
.build()
.map_err(|e| format!("tokio runtime: {e}"))?;
let url = url(&launch.listener);
std::thread::Builder::new()
.name("brep-mcp".into())
.spawn(move || {
rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<HostRequest>();
let q = queue.clone();
tokio::spawn(async move {
while let Some(req) = rx.recv().await {
let q = q.clone();
match req {
HostRequest::Command { envelope, reply } => {
tokio::spawn(async move {
let _ = reply.send(forward(&q, envelope).await);
});
}
HostRequest::Screenshot { region, reply } => {
tokio::spawn(async move {
let _ = reply.send(capture(&q, region).await);
});
}
HostRequest::Stop => {}
}
}
});
let info = HostInfo {
backend: "window",
adapter,
platform: std::env::consts::OS,
width: 0.0,
height: 0.0,
ppp: 1.0,
};
let server = BrepServer::new(launch.session_root, Backend::Attached { tx, info });
match server.attach(true).await {
Ok(info) => log::info!("brep-app --mcp: attached session {} ({}x{} @ {})", info.id, info.host.width, info.host.height, info.host.ppp),
Err(e) => {
eprintln!("brep-app --mcp: cannot attach the server to the app: {e}");
return;
}
}
let listener = match launch.listener.set_nonblocking(true).and_then(|_| tokio::net::TcpListener::from_std(launch.listener)) {
Ok(l) => l,
Err(e) => {
eprintln!("brep-app --mcp: listener: {e}");
return;
}
};
if let Err(e) = brep_mcp_core::http::serve_http(server, listener).await {
eprintln!("brep-app --mcp: server on {url} stopped: {e}");
}
});
})
.map_err(|e| format!("spawn the MCP server thread: {e}"))?;
Ok(())
}