1use crate::automation::command::{Envelope as AppEnvelope, Reply as AppReply};
15use crate::automation::queue::AutomationQueue;
16use brep_mcp_core::host::{Backend, Envelope, HostInfo, HostRequest, Reply};
17use brep_mcp_core::server::BrepServer;
18use serde_json::{json, Value};
19use std::net::TcpListener;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22use std::time::Duration;
23
24pub const DEFAULT_PORT: u16 = 8765;
26
27const REPLY_TIMEOUT: Duration = Duration::from_secs(120);
31
32pub struct Launch {
34 pub listener: TcpListener,
35 pub session_root: PathBuf,
36}
37
38pub fn bind(port: u16) -> Result<TcpListener, String> {
41 TcpListener::bind(("127.0.0.1", port)).map_err(|e| format!("cannot listen on 127.0.0.1:{port}: {e}"))
42}
43
44pub fn url(listener: &TcpListener) -> String {
45 let port = listener.local_addr().map(|a| a.port()).unwrap_or(DEFAULT_PORT);
46 brep_mcp_core::http::url(port)
47}
48
49pub fn default_session_root() -> PathBuf {
52 std::env::var_os("BREP_MCP_SESSION_ROOT")
53 .map(PathBuf::from)
54 .unwrap_or_else(|| std::env::temp_dir().join("brep-mcp"))
55}
56
57pub fn agent_instructions(url: &str, session_root: &Path) -> String {
60 format!(
61 "BREP MCP server listening on {url}\n\
62 This window is the session: an agent's tools act on the documents you see here.\n\
63 \n\
64 Claude Code:\n\
65 \x20 claude mcp add --transport http brep {url}\n\
66 \x20 or in a project's .mcp.json:\n\
67 \x20 {{ \"mcpServers\": {{ \"brep\": {{ \"type\": \"http\", \"url\": \"{url}\" }} }} }}\n\
68 \n\
69 Codex:\n\
70 \x20 codex mcp add brep --url {url}\n\
71 \x20 or in ~/.codex/config.toml:\n\
72 \x20 [mcp_servers.brep]\n\
73 \x20 url = \"{url}\"\n\
74 \n\
75 Then ask the agent to call session_start; screenshots and call logs land under\n\
76 \x20 {root}\n",
77 root = session_root.display(),
78 )
79}
80
81fn convert(mut r: AppReply) -> Reply {
83 let blob = r.blob.take();
84 let (id, frame) = (r.id, r.frame);
85 match serde_json::to_value(&r).map_err(|e| e.to_string()).and_then(|v| Reply::from_app(v, blob)) {
86 Ok(reply) => reply,
87 Err(e) => Reply::err(id, frame, e),
88 }
89}
90
91async fn forward(queue: &Arc<AutomationQueue>, envelope: Envelope) -> Reply {
93 let id = envelope.id;
94 let rx = queue.submit(AppEnvelope { id, cmd: envelope.cmd, args: envelope.args });
95 match tokio::task::spawn_blocking(move || rx.recv_timeout(REPLY_TIMEOUT)).await {
96 Ok(Ok(reply)) => convert(reply),
97 Ok(Err(std::sync::mpsc::RecvTimeoutError::Timeout)) => Reply::err(
98 id,
99 queue.frame(),
100 format!("no reply from the app within {} s (is the window responding?)", REPLY_TIMEOUT.as_secs()),
101 ),
102 Ok(Err(std::sync::mpsc::RecvTimeoutError::Disconnected)) => Reply::err(id, queue.frame(), "the app dropped the command"),
103 Err(e) => Reply::err(id, queue.frame(), e.to_string()),
104 }
105}
106
107async fn capture(queue: &Arc<AutomationQueue>, region: Value) -> Result<(Value, Vec<u8>), String> {
110 let reply = forward(queue, Envelope { id: 0, cmd: "screenshot".into(), args: json!({ "region": region }) }).await;
111 if !reply.ok {
112 return Err(reply.error.unwrap_or_else(|| "screenshot failed".into()));
113 }
114 let png = reply.blob.ok_or("the screenshot reply carried no image")?;
115 Ok((reply.result.unwrap_or(Value::Null), png))
116}
117
118pub fn start(queue: Arc<AutomationQueue>, adapter: String, launch: Launch) -> Result<(), String> {
122 let rt = tokio::runtime::Builder::new_multi_thread()
123 .worker_threads(2)
124 .thread_name("brep-mcp-worker")
125 .enable_all()
126 .build()
127 .map_err(|e| format!("tokio runtime: {e}"))?;
128 let url = url(&launch.listener);
129 std::thread::Builder::new()
130 .name("brep-mcp".into())
131 .spawn(move || {
132 rt.block_on(async move {
133 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<HostRequest>();
134 let q = queue.clone();
135 tokio::spawn(async move {
136 while let Some(req) = rx.recv().await {
137 let q = q.clone();
138 match req {
139 HostRequest::Command { envelope, reply } => {
140 tokio::spawn(async move {
141 let _ = reply.send(forward(&q, envelope).await);
142 });
143 }
144 HostRequest::Screenshot { region, reply } => {
145 tokio::spawn(async move {
146 let _ = reply.send(capture(&q, region).await);
147 });
148 }
149 HostRequest::Stop => {}
151 }
152 }
153 });
154 let info = HostInfo {
155 backend: "window",
156 adapter,
157 platform: std::env::consts::OS,
158 width: 0.0,
159 height: 0.0,
160 ppp: 1.0,
161 };
162 let server = BrepServer::new(launch.session_root, Backend::Attached { tx, info });
163 match server.attach(true).await {
166 Ok(info) => log::info!("brep-app --mcp: attached session {} ({}x{} @ {})", info.id, info.host.width, info.host.height, info.host.ppp),
167 Err(e) => {
168 eprintln!("brep-app --mcp: cannot attach the server to the app: {e}");
169 return;
170 }
171 }
172 let listener = match launch.listener.set_nonblocking(true).and_then(|_| tokio::net::TcpListener::from_std(launch.listener)) {
173 Ok(l) => l,
174 Err(e) => {
175 eprintln!("brep-app --mcp: listener: {e}");
176 return;
177 }
178 };
179 if let Err(e) = brep_mcp_core::http::serve_http(server, listener).await {
180 eprintln!("brep-app --mcp: server on {url} stopped: {e}");
181 }
182 });
183 })
184 .map_err(|e| format!("spawn the MCP server thread: {e}"))?;
185 Ok(())
186}
187
188