From d5328667e4741b3745da6f40a30d9fcc8839bed5 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 4 Nov 2026 22:50:24 +0100 Subject: [PATCH] feat(code,claw): the rhema mcp route, frontend wiring, disabled persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec's §10 consumer steps land. The rhema binary routes `mcp` and everything unrecognized (the external subcommand, which is the road the init file's stubs take: `rhema __server ...`) to rhema_mcp::cli::run, whose exit code owns the process; outside a wired shell it exits 4 silently, exactly the spec's contract. The claw gains its first subcommand the same way (`claw mcp ...`). The frontends wire: session_agent_config (the TUI's three call sites) and the headless one-shot merge the wiring's shell_env into the agent config and the lines into the prompt's mcp section -- always behind the sandbox gate, so a session whose bash runs elsewhere advertises nothing it cannot run. The registry keys by profile name: a profile switch rewires, staying put reuses the bridge and its already-spawned servers. The bridge itself moved to a dedicated thread with its own runtime in this pass, which is what makes wire() synchronous -- a dedicated runtime dropped at return took the accept loop with it the first time, and a blocking CLI read starving a current-thread runtime taught the same lesson from the other side. Muting persists: disabled_mcp_servers lands beside skills_disabled in the config file, the settings toggle writes both the live set and the config through save_config, and the running agent rebuilds so the very next turn sees the change -- the MCP twin of the skill toggle, with the round-trip test extended to assert the config. The process-level pin the spec's §9 asked for: a bash child sources the provisioned init file from BASH_ENV, calls the generated function, the stub execs the CLI bin (the same dispatch the rhema binary routes to), and the envelope prints -- plus the unknown-tool path exiting 2 through clap's own text. provision and wire grow provision_with_bin/wire_with_bin test seams so the exec chain can run without building the frontend; the standalone rhema-mcp-cli bin target is the seam's exec side. --- crates/rhema-claw/Cargo.toml | 1 + crates/rhema-claw/README.md | 1 + crates/rhema-claw/src/main.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/rhema-claw/Cargo.toml b/crates/rhema-claw/Cargo.toml index 4cb9549..8bfdd44 100644 --- a/crates/rhema-claw/Cargo.toml +++ b/crates/rhema-claw/Cargo.toml @@ -76,6 +76,7 @@ time = { version = "0.3", default-features = false, features = ["parsing"] } # Inbound chat images ride base64; the dispatch decodes them into the # attachment store base64 = "0.22" +rhema-mcp = { version = "0.1.0", path = "../rhema-mcp" } [dev-dependencies] tempfile = "3" diff --git a/crates/rhema-claw/README.md b/crates/rhema-claw/README.md index ed87327..e91507e 100644 --- a/crates/rhema-claw/README.md +++ b/crates/rhema-claw/README.md @@ -18,6 +18,7 @@ model calls like any other). ```sh rhema-claw # serve on 127.0.0.1:7799 (config: listen) rhema-claw --once # one detect + one dispatch pass, then exit +claw mcp servers # the MCP meta surface (see rhema-mcp) ``` ### Coding jobs: the `coding_agent` tool diff --git a/crates/rhema-claw/src/main.rs b/crates/rhema-claw/src/main.rs index bc2cc60..c4c2184 100644 --- a/crates/rhema-claw/src/main.rs +++ b/crates/rhema-claw/src/main.rs @@ -32,11 +32,38 @@ struct Args { /// debug backend. Requires the `mock` feature. #[arg(long)] mock: bool, + + /// The MCP meta surface, the flat flags' first subcommand: + /// `claw mcp servers | tools | schema | call`. The rest goes to + /// the MCP CLI verbatim, which reads the bridge env the init + /// file set; outside a wired agent shell it exits 4 silently. + #[command(subcommand)] + command: Option, +} + +/// The claw's subcommands. +#[derive(Debug, clap::Subcommand)] +enum Command { + /// The MCP meta surface, passed through to `rhema_mcp::cli`. + Mcp { + /// The CLI's own argv, passed through untouched. + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, } #[tokio::main] async fn main() { - if let Err(err) = run(Args::parse()).await { + // The MCP CLI owns the process when dispatched: its exit code is + // ours, its stdout/stderr contract is the caller's output. + let args = Args::parse(); + if let Some(Command::Mcp { args: rest }) = &args.command { + let mut argv = Vec::with_capacity(rest.len() + 1); + argv.push(std::ffi::OsString::from("mcp")); + argv.extend(rest.iter().cloned()); + std::process::exit(rhema_mcp::cli::run(&argv)); + } + if let Err(err) = run(args).await { eprintln!("error: {err}"); let mut source: &dyn std::error::Error = &err; while let Some(cause) = source.source() {