1use std::path::PathBuf;
7use std::str::FromStr;
8use std::sync::Arc;
9
10use async_process::Child;
11use std::pin::pin;
12
13use crate::schema::v1::{EnvVariable, McpServer as SchemaMcpServer, McpServerStdio};
14use crate::{Client, Conductor, Role};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum LineDirection {
19 Stdin,
21 Stdout,
23 Stderr,
25}
26
27pub struct AcpAgent {
60 server: SchemaMcpServer,
61 debug_callback: Option<Arc<dyn Fn(&str, LineDirection) + Send + Sync + 'static>>,
62}
63
64impl std::fmt::Debug for AcpAgent {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("AcpAgent")
67 .field("server", &self.server)
68 .field(
69 "debug_callback",
70 &self.debug_callback.as_ref().map(|_| "..."),
71 )
72 .finish()
73 }
74}
75
76impl AcpAgent {
77 #[must_use]
79 pub fn new(server: SchemaMcpServer) -> Self {
80 Self {
81 server,
82 debug_callback: None,
83 }
84 }
85
86 #[must_use]
89 pub fn claude_agent() -> Self {
90 Self::from_str("npx -y @agentclientprotocol/claude-agent-acp@latest")
91 .expect("valid bash command")
92 }
93
94 #[must_use]
97 pub fn codex() -> Self {
98 Self::from_str("npx -y @agentclientprotocol/codex-acp@latest").expect("valid bash command")
99 }
100
101 #[deprecated(
104 since = "1.2.0",
105 note = "the package moved to @agentclientprotocol/claude-agent-acp; use `AcpAgent::claude_agent()` instead"
106 )]
107 #[must_use]
108 pub fn zed_claude_code() -> Self {
109 Self::from_str("npx -y @zed-industries/claude-code-acp@latest").expect("valid bash command")
110 }
111
112 #[deprecated(
115 since = "1.2.0",
116 note = "the package moved to @agentclientprotocol/codex-acp; use `AcpAgent::codex()` instead"
117 )]
118 #[must_use]
119 pub fn zed_codex() -> Self {
120 Self::from_str("npx -y @zed-industries/codex-acp@latest").expect("valid bash command")
121 }
122
123 #[must_use]
126 pub fn google_gemini() -> Self {
127 Self::from_str("npx -y -- @google/gemini-cli@latest --experimental-acp")
128 .expect("valid bash command")
129 }
130
131 #[must_use]
133 pub fn server(&self) -> &SchemaMcpServer {
134 &self.server
135 }
136
137 #[must_use]
139 pub fn into_server(self) -> SchemaMcpServer {
140 self.server
141 }
142
143 #[must_use]
160 pub fn with_debug<F>(mut self, callback: F) -> Self
161 where
162 F: Fn(&str, LineDirection) + Send + Sync + 'static,
163 {
164 self.debug_callback = Some(Arc::new(callback));
165 self
166 }
167
168 pub fn spawn_process(
171 &self,
172 ) -> Result<
173 (
174 async_process::ChildStdin,
175 async_process::ChildStdout,
176 async_process::ChildStderr,
177 Child,
178 ),
179 crate::Error,
180 > {
181 match &self.server {
182 SchemaMcpServer::Stdio(stdio) => {
183 let mut cmd = async_process::Command::new(&stdio.command);
184 cmd.args(&stdio.args);
185 for env_var in &stdio.env {
186 cmd.env(&env_var.name, &env_var.value);
187 }
188 #[cfg(windows)]
189 {
190 use async_process::windows::CommandExt as _;
191
192 cmd.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW);
193 }
194 cmd.stdin(std::process::Stdio::piped())
195 .stdout(std::process::Stdio::piped())
196 .stderr(std::process::Stdio::piped());
197
198 let mut child = cmd.spawn().map_err(crate::Error::into_internal_error)?;
199
200 let child_stdin = child
201 .stdin
202 .take()
203 .ok_or_else(|| crate::util::internal_error("Failed to open stdin"))?;
204 let child_stdout = child
205 .stdout
206 .take()
207 .ok_or_else(|| crate::util::internal_error("Failed to open stdout"))?;
208 let child_stderr = child
209 .stderr
210 .take()
211 .ok_or_else(|| crate::util::internal_error("Failed to open stderr"))?;
212
213 Ok((child_stdin, child_stdout, child_stderr, child))
214 }
215 SchemaMcpServer::Http(_) => Err(crate::util::internal_error(
216 "HTTP transport not yet supported by AcpAgent",
217 )),
218 SchemaMcpServer::Sse(_) => Err(crate::util::internal_error(
219 "SSE transport not yet supported by AcpAgent",
220 )),
221 _ => Err(crate::util::internal_error(
222 "Unknown MCP server transport type",
223 )),
224 }
225 }
226}
227
228struct ChildGuard(Child);
230
231impl ChildGuard {
232 async fn wait(&mut self) -> std::io::Result<std::process::ExitStatus> {
233 self.0.status().await
234 }
235}
236
237impl Drop for ChildGuard {
238 fn drop(&mut self) {
239 drop(self.0.kill());
240 }
241}
242
243async fn monitor_child(
248 child: Child,
249 stderr_rx: futures::channel::oneshot::Receiver<String>,
250) -> Result<(), crate::Error> {
251 let mut guard = ChildGuard(child);
252
253 let status = guard
254 .wait()
255 .await
256 .map_err(|e| crate::util::internal_error(format!("Failed to wait for process: {e}")))?;
257
258 if status.success() {
259 Ok(())
260 } else {
261 let stderr = stderr_rx.await.unwrap_or_default();
262
263 let message = if stderr.is_empty() {
264 format!("Process exited with {status}")
265 } else {
266 format!("Process exited with {status}: {stderr}")
267 };
268
269 Err(crate::util::internal_error(message))
270 }
271}
272
273pub trait AcpAgentCounterpartRole: Role {}
275
276impl AcpAgentCounterpartRole for Client {}
277
278impl AcpAgentCounterpartRole for Conductor {}
279
280impl<Counterpart: AcpAgentCounterpartRole> crate::ConnectTo<Counterpart> for AcpAgent {
281 async fn connect_to(
282 self,
283 client: impl crate::ConnectTo<Counterpart::Counterpart>,
284 ) -> Result<(), crate::Error> {
285 use futures::io::BufReader;
286 use futures::{AsyncBufReadExt, AsyncWriteExt, StreamExt};
287
288 let (child_stdin, child_stdout, child_stderr, child) = self.spawn_process()?;
289
290 let (stderr_tx, stderr_rx) = futures::channel::oneshot::channel::<String>();
292
293 let debug_callback = self.debug_callback.clone();
297 let stderr_future = async move {
298 let stderr_reader = BufReader::new(child_stderr);
299 let mut stderr_lines = stderr_reader.lines();
300 let mut collected = String::new();
301 while let Some(line_result) = stderr_lines.next().await {
302 if let Ok(line) = line_result {
303 if let Some(ref callback) = debug_callback {
304 callback(&line, LineDirection::Stderr);
305 }
306 if !collected.is_empty() {
307 collected.push('\n');
308 }
309 collected.push_str(&line);
310 }
311 }
312 drop(stderr_tx.send(collected));
313 };
314
315 let child_monitor = monitor_child(child, stderr_rx);
317
318 let incoming_lines: std::pin::Pin<
320 Box<dyn futures::Stream<Item = std::io::Result<String>> + Send>,
321 > = if let Some(callback) = self.debug_callback.clone() {
322 Box::pin(BufReader::new(child_stdout).lines().inspect(move |result| {
323 if let Ok(line) = result {
324 callback(line, LineDirection::Stdout);
325 }
326 }))
327 } else {
328 Box::pin(BufReader::new(child_stdout).lines())
329 };
330
331 let outgoing_sink: std::pin::Pin<
333 Box<dyn futures::Sink<String, Error = std::io::Error> + Send>,
334 > = if let Some(callback) = self.debug_callback.clone() {
335 Box::pin(futures::sink::unfold(
336 (child_stdin, callback),
337 async move |(mut writer, callback), line: String| {
338 callback(&line, LineDirection::Stdin);
339 let mut bytes = line.into_bytes();
340 bytes.push(b'\n');
341 writer.write_all(&bytes).await?;
342 Ok::<_, std::io::Error>((writer, callback))
343 },
344 ))
345 } else {
346 Box::pin(futures::sink::unfold(
347 child_stdin,
348 async move |mut writer, line: String| {
349 let mut bytes = line.into_bytes();
350 bytes.push(b'\n');
351 writer.write_all(&bytes).await?;
352 Ok::<_, std::io::Error>(writer)
353 },
354 ))
355 };
356
357 let protocol_future = crate::ConnectTo::<Counterpart>::connect_to(
360 crate::Lines::new(outgoing_sink, incoming_lines),
361 client,
362 );
363
364 let stderr_future = pin!(stderr_future);
365 let protocol_future = pin!(protocol_future);
366 let child_monitor = pin!(child_monitor);
367
368 let main_race = async {
370 match futures::future::select(protocol_future, child_monitor).await {
371 futures::future::Either::Left((result, _))
372 | futures::future::Either::Right((result, _)) => result,
373 }
374 };
375
376 let main_race = pin!(main_race);
379 match futures::future::select(main_race, stderr_future).await {
380 futures::future::Either::Left((result, _)) => result,
381 futures::future::Either::Right(((), protocol)) => protocol.await,
382 }
383 }
384}
385
386impl AcpAgent {
387 pub fn from_args<I, T>(args: I) -> Result<Self, crate::Error>
405 where
406 I: IntoIterator<Item = T>,
407 T: ToString,
408 {
409 let args: Vec<String> = args.into_iter().map(|s| s.to_string()).collect();
410
411 if args.is_empty() {
412 return Err(crate::util::internal_error("Arguments cannot be empty"));
413 }
414
415 let mut env = vec![];
416 let mut command_idx = 0;
417
418 for (i, arg) in args.iter().enumerate() {
419 if let Some((name, value)) = parse_env_var(arg) {
420 env.push(EnvVariable::new(name, value));
421 command_idx = i + 1;
422 } else {
423 break;
424 }
425 }
426
427 if command_idx >= args.len() {
428 return Err(crate::util::internal_error(
429 "No command found (only environment variables provided)",
430 ));
431 }
432
433 let command = PathBuf::from(&args[command_idx]);
434 let cmd_args = args[command_idx + 1..].to_vec();
435
436 let name = command
437 .file_name()
438 .and_then(|n| n.to_str())
439 .unwrap_or("agent")
440 .to_string();
441
442 Ok(AcpAgent {
443 server: SchemaMcpServer::Stdio(
444 McpServerStdio::new(name, command).args(cmd_args).env(env),
445 ),
446 debug_callback: None,
447 })
448 }
449}
450
451fn parse_env_var(s: &str) -> Option<(String, String)> {
453 let eq_pos = s.find('=')?;
454 if eq_pos == 0 {
455 return None;
456 }
457
458 let name = &s[..eq_pos];
459 let value = &s[eq_pos + 1..];
460
461 let mut chars = name.chars();
462 let first = chars.next()?;
463 if !first.is_ascii_alphabetic() && first != '_' {
464 return None;
465 }
466 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
467 return None;
468 }
469
470 Some((name.to_string(), value.to_string()))
471}
472
473impl FromStr for AcpAgent {
474 type Err = crate::Error;
475
476 fn from_str(s: &str) -> Result<Self, Self::Err> {
477 let trimmed = s.trim();
478
479 if trimmed.starts_with('{') {
480 let server: SchemaMcpServer = serde_json::from_str(trimmed)
481 .map_err(|e| crate::util::internal_error(format!("Failed to parse JSON: {e}")))?;
482 return Ok(Self {
483 server,
484 debug_callback: None,
485 });
486 }
487
488 let parts = shell_words::split(trimmed)
489 .map_err(|e| crate::util::internal_error(format!("Failed to parse command: {e}")))?;
490
491 Self::from_args(parts)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_parse_simple_command() {
501 let agent = AcpAgent::from_str("python agent.py").unwrap();
502 match agent.server {
503 SchemaMcpServer::Stdio(stdio) => {
504 assert_eq!(stdio.name, "python");
505 assert_eq!(stdio.command, PathBuf::from("python"));
506 assert_eq!(stdio.args, vec!["agent.py"]);
507 assert!(stdio.env.is_empty());
508 }
509 _ => panic!("Expected Stdio variant"),
510 }
511 }
512
513 #[test]
514 fn test_parse_command_with_args() {
515 let agent = AcpAgent::from_str("node server.js --port 8080 --verbose").unwrap();
516 match agent.server {
517 SchemaMcpServer::Stdio(stdio) => {
518 assert_eq!(stdio.name, "node");
519 assert_eq!(stdio.command, PathBuf::from("node"));
520 assert_eq!(stdio.args, vec!["server.js", "--port", "8080", "--verbose"]);
521 assert!(stdio.env.is_empty());
522 }
523 _ => panic!("Expected Stdio variant"),
524 }
525 }
526
527 #[test]
528 fn test_parse_command_with_quotes() {
529 let agent = AcpAgent::from_str(r#"python "my agent.py" --name "Test Agent""#).unwrap();
530 match agent.server {
531 SchemaMcpServer::Stdio(stdio) => {
532 assert_eq!(stdio.name, "python");
533 assert_eq!(stdio.command, PathBuf::from("python"));
534 assert_eq!(stdio.args, vec!["my agent.py", "--name", "Test Agent"]);
535 assert!(stdio.env.is_empty());
536 }
537 _ => panic!("Expected Stdio variant"),
538 }
539 }
540
541 #[test]
542 fn test_parse_json_stdio() {
543 let json = r#"{
544 "type": "stdio",
545 "name": "my-agent",
546 "command": "/usr/bin/python",
547 "args": ["agent.py", "--verbose"],
548 "env": []
549 }"#;
550 let agent = AcpAgent::from_str(json).unwrap();
551 match agent.server {
552 SchemaMcpServer::Stdio(stdio) => {
553 assert_eq!(stdio.name, "my-agent");
554 assert_eq!(stdio.command, PathBuf::from("/usr/bin/python"));
555 assert_eq!(stdio.args, vec!["agent.py", "--verbose"]);
556 assert!(stdio.env.is_empty());
557 }
558 _ => panic!("Expected Stdio variant"),
559 }
560 }
561
562 #[test]
563 fn test_parse_json_http() {
564 let json = r#"{
565 "type": "http",
566 "name": "remote-agent",
567 "url": "https://example.com/agent",
568 "headers": []
569 }"#;
570 let agent = AcpAgent::from_str(json).unwrap();
571 match agent.server {
572 SchemaMcpServer::Http(http) => {
573 assert_eq!(http.name, "remote-agent");
574 assert_eq!(http.url, "https://example.com/agent");
575 assert!(http.headers.is_empty());
576 }
577 _ => panic!("Expected Http variant"),
578 }
579 }
580}