diff --git a/.env.example b/.env.example index db5a7ea25c..c0b29558dc 100644 --- a/.env.example +++ b/.env.example @@ -140,6 +140,11 @@ RUST_LOG=buzz_relay=debug,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_ # Binary for an optional MCP server sidecar (e.g. buzz-dev-mcp for buzz-agent). # BUZZ_ACP_MCP_COMMAND= +# Comma-separated arguments passed to the MCP server binary. Needed by servers +# that are launched through a runner rather than as a bare binary, for example +# "npx" with args "-y,some-mcp-server". +# BUZZ_ACP_MCP_ARGS= + # Number of parallel agent subprocesses (1–32). # BUZZ_ACP_AGENTS=1 diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index d9cd362cb8..813aadb46e 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -102,6 +102,7 @@ All configuration is via environment variables (or CLI flags — every env var h | `BUZZ_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. | | `BUZZ_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). | | `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. | +| `BUZZ_ACP_MCP_ARGS` | no | `""` (empty) | MCP server arguments (comma-separated). Lets a server be launched through a runner, e.g. command `npx` with args `-y,some-mcp-server`. | | `BUZZ_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. | | `BUZZ_ACP_MAX_TURN_DURATION` | no | `7200` | Absolute wall-clock cap per turn (safety valve). | | `BUZZ_API_TOKEN` | no | — | API token (required if relay enforces token auth). | diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index a38d6faa14..02b90f5360 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -261,6 +261,15 @@ pub struct CliArgs { #[arg(long, env = "BUZZ_ACP_MCP_COMMAND", default_value = "")] pub mcp_command: String, + /// Arguments passed to the MCP server binary. + #[arg( + long, + env = "BUZZ_ACP_MCP_ARGS", + default_value = "", + value_delimiter = ',' + )] + pub mcp_args: Vec, + /// Idle timeout: max seconds of silence before killing a turn. /// Resets on any agent stdout activity. #[arg(long, env = "BUZZ_ACP_IDLE_TIMEOUT")] @@ -489,6 +498,7 @@ pub struct Config { pub agent_command: String, pub agent_args: Vec, pub mcp_command: String, + pub mcp_args: Vec, pub idle_timeout_secs: u64, pub max_turn_duration_secs: u64, pub agents: u32, @@ -964,6 +974,7 @@ impl Config { agent_command, agent_args, mcp_command: args.mcp_command, + mcp_args: args.mcp_args, idle_timeout_secs, max_turn_duration_secs, agents: args.agents, @@ -1024,12 +1035,13 @@ impl Config { format!(" allowed_respond_to=[{}]", modes.join(",")) }; format!( - "relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}{}", + "relay={} pubkey={} agent_cmd={} {} mcp_cmd={} {} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}{}", self.relay_url, self.keys.public_key().to_hex(), self.agent_command, self.agent_args.join(" "), self.mcp_command, + self.mcp_args.join(" "), self.idle_timeout_secs, self.max_turn_duration_secs, self.agents, @@ -1338,6 +1350,7 @@ mod tests { agent_command: "goose".into(), agent_args: vec!["acp".into()], mcp_command: "".into(), + mcp_args: vec![], idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: DEFAULT_MAX_TURN_DURATION_SECS, agents: 1, diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 03b75a4211..447b9818f3 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -4105,7 +4105,7 @@ fn build_mcp_servers(config: &Config) -> Vec { .unwrap_or("mcp") .to_string(), command: config.mcp_command.clone(), - args: vec![], + args: config.mcp_args.clone(), env: { let mut env = vec![ EnvVar { @@ -4906,6 +4906,7 @@ mod build_mcp_servers_tests { agent_command: "goose".into(), agent_args: vec!["acp".into()], mcp_command: "test-mcp-server".into(), + mcp_args: vec![], idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS, agents: 1, @@ -5035,6 +5036,40 @@ mod build_mcp_servers_tests { "Path::new(\".\").file_stem() is None — should fall back to \"mcp\"" ); } + + #[test] + fn mcp_args_default_to_empty() { + let mut config = test_config(); + config.mcp_command = "/opt/bin/my-mcp-server".into(); + config.mcp_args = vec![]; + let servers = build_mcp_servers(&config); + assert_eq!(servers.len(), 1); + assert!( + servers[0].args.is_empty(), + "an MCP command with no args should spawn with no args" + ); + } + + #[test] + fn mcp_args_are_passed_to_the_server() { + let mut config = test_config(); + config.mcp_command = "npx".into(); + config.mcp_args = vec!["-y".into(), "some-mcp-server".into()]; + let servers = build_mcp_servers(&config); + assert_eq!(servers.len(), 1); + assert_eq!(servers[0].command, "npx"); + assert_eq!(servers[0].args, vec!["-y", "some-mcp-server"]); + } + + #[test] + fn mcp_args_do_not_affect_the_derived_server_name() { + // The name still comes from the command's file stem, not the args. + let mut config = test_config(); + config.mcp_command = "/opt/bin/my-mcp-server".into(); + config.mcp_args = vec!["--port".into(), "8080".into()]; + let servers = build_mcp_servers(&config); + assert_eq!(servers[0].name, "my-mcp-server"); + } } #[cfg(test)] @@ -5072,6 +5107,7 @@ mod error_outcome_emission_tests { agent_command: "true".into(), agent_args: vec![], mcp_command: "test-mcp-server".into(), + mcp_args: vec![], idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS, agents: 1,