diff --git a/config.toml.example b/config.toml.example index aa62c0a84..e06ac0c0d 100644 --- a/config.toml.example +++ b/config.toml.example @@ -387,6 +387,9 @@ error_hold_ms = 2500 # ~/.openab/agent/mcp.json. Any coding CLI on this host (Kiro, Claude Code, # Codex, ...) can connect to http://127.0.0.1:8848/mcp. # Presence of [mcp] enables the listener; omit the section to disable. +# With no chat adapter configured, [mcp] alone is a valid config: openab +# runs in facade-only mode (serves just the MCP listener) — for +# coding-CLI-only hosts, dev loops, and CI runners (#1451). # NOTE: the endpoint has no authentication — it must stay on loopback # (non-loopback addresses are refused at startup). # [mcp] diff --git a/openab-agent/src/main.rs b/openab-agent/src/main.rs index 04a5428b6..555e535fd 100644 --- a/openab-agent/src/main.rs +++ b/openab-agent/src/main.rs @@ -29,17 +29,6 @@ enum Commands { #[command(subcommand)] action: McpAction, }, - /// Serve the OAB MCP Facade over loopback Streamable HTTP: an inbound - /// MCP server exposing `search_capabilities` / `execute_capability` - /// backed by the configured downstream MCP servers (global + project - /// mcp.json). The OAB broker hosts the same facade in-process when - /// `[mcp]` is set in config.toml; this subcommand is the standalone way - /// to run it. - McpFacade { - /// Loopback address to listen on (non-loopback addresses are refused). - #[arg(long, default_value = "127.0.0.1:8848")] - listen: String, - }, } #[derive(Subcommand)] @@ -171,12 +160,6 @@ async fn main() { } } }, - Some(Commands::McpFacade { listen }) => { - if let Err(e) = mcp::facade::serve_http(&listen).await { - eprintln!("mcp-facade: {e:#}"); - std::process::exit(1); - } - } } } diff --git a/src/main.rs b/src/main.rs index 63072b76d..c19962067 100644 --- a/src/main.rs +++ b/src/main.rs @@ -326,8 +326,25 @@ async fn main() -> anyhow::Result<()> { && cfg.telegram.is_none() && !has_unified_platform(&cfg) { + // Facade-only run mode (#1451): an adapter-less config with `[mcp]` + // present is a valid deployment — the broker serves just the OAB MCP + // Facade listener. One entrypoint, config-driven: hosts that only + // need the capability surface (coding-CLI-only users, dev loops, CI + // runners, agent hosts with no chat platform) run the same + // `openab run` with a two-line config instead of a chat token. + if let Some(mcp_cfg) = cfg.mcp.clone() { + tracing::info!( + listen = %mcp_cfg.listen, + "no chat adapter configured — running in facade-only mode ([mcp] present)" + ); + // Foreground, not spawned: the facade IS the workload. A bind + // failure or server exit terminates the process (fail fast). + return openab_mcp::mcp::facade::serve_http(&mcp_cfg.listen) + .await + .map_err(|e| anyhow::anyhow!("OAB MCP facade exited: {e:#}")); + } anyhow::bail!( - "no adapter configured — add [discord], [slack], [telegram], [wecom], [googlechat], or [gateway] to config, or set platform env vars (TELEGRAM_BOT_TOKEN, etc.)" + "no adapter configured — add [discord], [slack], [telegram], [wecom], [googlechat], or [gateway] to config (or [mcp] for facade-only mode), or set platform env vars (TELEGRAM_BOT_TOKEN, etc.)" ); }