feat(code,claw): the rhema mcp route, frontend wiring, disabled persistence

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 <name> ...`) 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.
This commit is contained in:
vhaudiquet committed 2026-11-04 22:58:55 +01:00
1 parent abcafccc8e
commit d5328667e4
3 files changed
+30 -1

No files matched your search

+1
View File
@@ -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"
+1
View File
@@ -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
+28 -1
View File
@@ -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<Command>,
}
/// 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<std::ffi::OsString>,
},
}
#[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() {