diff --git a/tui/src/app.rs b/tui/src/app.rs index 2e3c0c9..39f6427 100644 --- a/tui/src/app.rs +++ b/tui/src/app.rs @@ -887,6 +887,13 @@ impl App { Ok(id) } + /// Absolute path to this repo's `.mcp.json` if present — passed to claude so + /// its MCP servers load trusted (no discovery prompt). + fn mcp_config_path(&self) -> Option { + let p = self.store.repo_root.join(".mcp.json"); + if p.is_file() { Some(p.to_string_lossy().into_owned()) } else { None } + } + /// Launch an interactive PTY agent from the form. Task is required. pub fn spawn_from_form(&mut self) { if self.new_form.task_text().trim().is_empty() { @@ -925,7 +932,8 @@ impl App { ) }; - let argv = interactive_argv(&harness, &model); + let mcp = self.mcp_config_path(); + let argv = interactive_argv(&harness, &model, mcp.as_deref()); let task_text = self.new_form.task_text(); match AgentProc::spawn( id.clone(), harness.clone(), model.clone(), prompt, worktree.clone(), &argv, rows, cols, @@ -973,7 +981,8 @@ impl App { fn resume(&mut self, id: &str) { let Some(rec) = self.registry.iter().find(|r| r.id == id).cloned() else { return }; let (cols, rows) = self.term_size; - let argv = resume_argv(&rec.harness, &rec.model); + let mcp = self.mcp_config_path(); + let argv = resume_argv(&rec.harness, &rec.model, mcp.as_deref()); // no task seed on resume — the harness restores the prior conversation match AgentProc::spawn( rec.id.clone(), rec.harness.clone(), rec.model.clone(), String::new(), @@ -1264,16 +1273,33 @@ fn merge_keep(mut rows: Vec) -> Vec { rows } -/// argv for a harness's INTERACTIVE REPL. Overridable via -/// `NEXUM_INTERACTIVE_CMD_` (whitespace-split) so tests inject a stub. -pub fn interactive_argv(harness: &str, model: &str) -> Vec { +/// Claude MCP flags: load the delegation server explicitly from `mcp` and +/// ignore auto-discovered project/user configs. Passing the server on the CLI +/// marks it trusted, so the "found new MCP in project" prompt never appears (it +/// would otherwise swallow our seeded first message). Empty when there's no +/// `.mcp.json` — then nothing is discovered, so no prompt either. +fn claude_mcp_flags(mcp: Option<&str>) -> Vec { + match mcp { + Some(path) => vec!["--mcp-config".into(), path.into(), "--strict-mcp-config".into()], + None => Vec::new(), + } +} + +/// argv for a harness's INTERACTIVE REPL. `mcp` = path to a `.mcp.json` to load +/// explicitly (claude). Overridable via `NEXUM_INTERACTIVE_CMD_` +/// (whitespace-split) so tests inject a stub. +pub fn interactive_argv(harness: &str, model: &str, mcp: Option<&str>) -> Vec { if let Ok(over) = std::env::var(format!("NEXUM_INTERACTIVE_CMD_{}", harness.to_uppercase())) { if !over.trim().is_empty() { return over.split_whitespace().map(|s| s.to_string()).collect(); } } match harness { - "claude" => vec!["claude".into(), "--model".into(), model.into()], + "claude" => { + let mut v = vec!["claude".into(), "--model".into(), model.into()]; + v.extend(claude_mcp_flags(mcp)); + v + } "opencode" => vec!["opencode".into()], "cursor" => vec!["cursor-agent".into(), "--model".into(), model.into()], _ => vec!["claude".into()], @@ -1282,17 +1308,21 @@ pub fn interactive_argv(harness: &str, model: &str) -> Vec { /// argv to RESUME a harness in an existing worktree (harness restores the prior /// session). Overridable via `NEXUM_RESUME_CMD_`. -pub fn resume_argv(harness: &str, model: &str) -> Vec { +pub fn resume_argv(harness: &str, model: &str, mcp: Option<&str>) -> Vec { if let Ok(over) = std::env::var(format!("NEXUM_RESUME_CMD_{}", harness.to_uppercase())) { if !over.trim().is_empty() { return over.split_whitespace().map(|s| s.to_string()).collect(); } } match harness { - "claude" => vec!["claude".into(), "--continue".into(), "--model".into(), model.into()], + "claude" => { + let mut v = vec!["claude".into(), "--continue".into(), "--model".into(), model.into()]; + v.extend(claude_mcp_flags(mcp)); + v + } "opencode" => vec!["opencode".into(), "--continue".into()], "cursor" => vec!["cursor-agent".into(), "--resume".into()], - _ => interactive_argv(harness, model), + _ => interactive_argv(harness, model, mcp), } } @@ -1685,6 +1715,15 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn claude_mcp_flags_load_explicitly_and_strict() { + // with a .mcp.json → pass it on the CLI (trusted, no prompt) + strict + let f = claude_mcp_flags(Some("/repo/.mcp.json")); + assert_eq!(f, vec!["--mcp-config", "/repo/.mcp.json", "--strict-mcp-config"]); + // without one → no MCP flags (nothing discovered, no prompt) + assert!(claude_mcp_flags(None).is_empty()); + } + #[test] fn on_path_detects_binaries() { assert!(on_path("sh"), "sh should be on PATH");