Skip to main content

browser_control/cli/
mcp.rs

1//! `browser-control mcp` subcommand entry point.
2
3use anyhow::{anyhow, Result};
4
5use crate::cli::env_resolver::{self, ResolvedBrowser};
6use crate::mcp::server::{run, ServerState, ToolRegistry};
7use crate::registry::Registry;
8
9/// Entry point for `browser-control mcp`.
10pub async fn run_cli(
11    browser_arg: Option<String>,
12    playwright_version: Option<String>,
13) -> Result<()> {
14    let resolved = resolve_browser(browser_arg).await?;
15    let sidecar_config = crate::sidecar::SidecarConfig {
16        version: playwright_version,
17    };
18    let state = ServerState::with_sidecar_config(resolved, sidecar_config);
19    let tools = ToolRegistry::new();
20    crate::mcp::tools::register_all(&tools);
21    run(state, tools).await
22}
23
24/// Resolution order: positional arg / `BROWSER_CONTROL` env (arg wins, env is
25/// the fallback — both are merged by clap into `browser_arg`) > persisted
26/// default (`browser-control set default ...`) > error.
27///
28/// We deliberately do NOT fall back to a "most recently alive" registry row:
29/// that hides which browser is being controlled and depends on global state
30/// that other processes can mutate, producing surprising results for agents
31/// that share a host.
32pub async fn resolve_browser(browser_arg: Option<String>) -> Result<ResolvedBrowser> {
33    let registry = Registry::open()?;
34    if let Some(arg) = browser_arg.as_deref().filter(|s| !s.is_empty()) {
35        let sel = env_resolver::parse(arg)?;
36        return env_resolver::resolve(sel, &registry).await;
37    }
38    if let Some(value) = crate::config::load()?.default {
39        let sel = env_resolver::parse(&value)?;
40        return env_resolver::resolve(sel, &registry).await;
41    }
42    Err(anyhow!(
43        "no browser selected: pass a browser argument, set BROWSER_CONTROL, or run `browser-control set default <value>`"
44    ))
45}
46
47/// Acquire the Firefox BiDi single-session lock if `resolved` is a
48/// registered browser on the BiDi engine. Returns `None` for external
49/// URL endpoints (the lock is per-registered-name) and for CDP engines.
50/// Wait bound: 30 s; on timeout, returns the typed `BidiLockBusy` error.
51pub fn acquire_bidi_lock_if_needed(
52    registry: &crate::registry::Registry,
53    resolved: &ResolvedBrowser,
54) -> Result<Option<crate::registry::BidiLockGuard>> {
55    use crate::detect::Engine;
56    if resolved.engine != Engine::Bidi {
57        return Ok(None);
58    }
59    let name = match &resolved.source {
60        crate::cli::env_resolver::Source::Registered { name } => name.clone(),
61        crate::cli::env_resolver::Source::External => return Ok(None),
62    };
63    let guard = registry.bidi_lock_acquire(&name, std::time::Duration::from_secs(30))?;
64    Ok(Some(guard))
65}