browser_control/mcp/
playwright.rs1use anyhow::{anyhow, Context, Result};
8use std::process::Stdio;
9use tokio::io::{AsyncRead, AsyncWrite};
10use tokio::process::Command;
11
12use crate::cli::env_resolver::ResolvedBrowser;
13
14pub async fn run(browser: &ResolvedBrowser) -> Result<i32> {
22 run_with_streams(
23 browser,
24 tokio::io::stdin(),
25 tokio::io::stdout(),
26 tokio::io::stderr(),
27 None,
28 )
29 .await
30}
31
32pub async fn run_with_streams<I, O, E>(
34 browser: &ResolvedBrowser,
35 mut stdin: I,
36 mut stdout: O,
37 mut stderr: E,
38 override_cmd: Option<(String, Vec<String>)>,
39) -> Result<i32>
40where
41 I: AsyncRead + Unpin + Send + 'static,
42 O: AsyncWrite + Unpin + Send + 'static,
43 E: AsyncWrite + Unpin + Send + 'static,
44{
45 let (program, args) = match override_cmd {
46 Some(c) => c,
47 None => choose_launcher(browser)?,
48 };
49
50 let mut child = Command::new(&program)
51 .args(&args)
52 .stdin(Stdio::piped())
53 .stdout(Stdio::piped())
54 .stderr(Stdio::piped())
55 .kill_on_drop(true)
56 .spawn()
57 .with_context(|| format!("failed to spawn `{program}`. Install Node.js (npm) or Bun."))?;
58
59 let mut child_stdin = child
60 .stdin
61 .take()
62 .ok_or_else(|| anyhow!("no child stdin"))?;
63 let mut child_stdout = child
64 .stdout
65 .take()
66 .ok_or_else(|| anyhow!("no child stdout"))?;
67 let mut child_stderr = child
68 .stderr
69 .take()
70 .ok_or_else(|| anyhow!("no child stderr"))?;
71
72 let in_to_child = tokio::spawn(async move {
73 let _ = tokio::io::copy(&mut stdin, &mut child_stdin).await;
74 });
75 let out_to_us = tokio::spawn(async move {
76 let _ = tokio::io::copy(&mut child_stdout, &mut stdout).await;
77 });
78 let err_to_us = tokio::spawn(async move {
79 let _ = tokio::io::copy(&mut child_stderr, &mut stderr).await;
80 });
81
82 let status = child.wait().await?;
83 in_to_child.abort();
84 out_to_us.await.ok();
85 err_to_us.await.ok();
86
87 Ok(status.code().unwrap_or(0))
88}
89
90fn choose_launcher(browser: &ResolvedBrowser) -> Result<(String, Vec<String>)> {
91 let endpoint = browser.endpoint.clone();
92 let args = vec![
93 "-y".into(),
94 "@playwright/mcp@latest".into(),
95 "--cdp-endpoint".into(),
96 endpoint,
97 ];
98 if which::which("npx").is_ok() {
99 Ok(("npx".into(), args))
100 } else if which::which("bunx").is_ok() {
101 Ok(("bunx".into(), args))
102 } else {
103 Err(anyhow!("neither `npx` nor `bunx` is on PATH. Install Node.js (https://nodejs.org/) or Bun (https://bun.sh/)."))
104 }
105}