Skip to content

Commit e322b7f

Browse files
committed
Cache MCP discovery and honor provider binary settings
- Reuse MCP discovery across settings and composer views - Preserve disabled server state and load Claude health asynchronously - Execute MCP commands with configured provider binaries
1 parent 6e58a42 commit e322b7f

11 files changed

Lines changed: 644 additions & 160 deletions

File tree

‎src-tauri/src/harness.rs‎

Lines changed: 167 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ pub struct HarnessHost {
121121
}
122122

123123
impl HarnessHost {
124+
pub(crate) fn runtime_binary_path(&self, provider: &str) -> Option<String> {
125+
self.runtime_binary_paths
126+
.lock()
127+
.unwrap_or_else(|e| e.into_inner())
128+
.as_ref()?
129+
.get(provider)
130+
.map(|path| path.trim().to_owned())
131+
.filter(|path| !path.is_empty())
132+
}
133+
124134
pub(crate) fn has_working_dir(&self, path: &Path) -> bool {
125135
self.lock_inner()
126136
.children
@@ -361,8 +371,24 @@ pub fn harness_resolve_claude() -> Result<CursorBinary, String> {
361371
})
362372
}
363373

364-
fn claude_mcp_command(args: Vec<String>, cwd: String, timeout: Duration) -> Result<String, String> {
365-
let binary = resolve_claude().ok_or("Claude Code CLI not found")?;
374+
fn resolve_mcp_binary(provider: &str, binary_path: Option<&str>) -> Result<PathBuf, String> {
375+
if !matches!(provider, "claude" | "codex" | "cursor" | "opencode") {
376+
return Err("Unsupported MCP provider".into());
377+
}
378+
match binary_path {
379+
Some(path) => resolve_harness_binary_override(provider, path),
380+
None => resolve_harness_binary_default(provider)
381+
.ok_or_else(|| format!("{provider} CLI not found")),
382+
}
383+
}
384+
385+
fn claude_mcp_command(
386+
args: Vec<String>,
387+
cwd: String,
388+
timeout: Duration,
389+
binary_path: Option<&str>,
390+
) -> Result<String, String> {
391+
let binary = resolve_mcp_binary("claude", binary_path)?;
366392
mcp_command(binary, args, cwd, timeout)
367393
}
368394

@@ -376,27 +402,7 @@ fn mcp_command(
376402
if !workdir.is_dir() {
377403
return Err("Project directory does not exist".into());
378404
}
379-
let mut command = Command::new(&binary);
380-
command
381-
.args(&args)
382-
.current_dir(workdir)
383-
.stdin(Stdio::null())
384-
.stdout(Stdio::piped())
385-
.stderr(Stdio::piped());
386-
prepare_child(&mut command, &binary.to_string_lossy());
387-
let child = spawn_managed(&mut command).map_err(|e| e.to_string())?;
388-
let pid = child.id();
389-
let (sender, receiver) = mpsc::channel();
390-
thread::spawn(move || {
391-
let _ = sender.send(child.wait_with_output());
392-
});
393-
let output = match receiver.recv_timeout(timeout) {
394-
Ok(result) => result.map_err(|e| e.to_string())?,
395-
Err(_) => {
396-
terminate(pid);
397-
return Err("Claude MCP command timed out".into());
398-
}
399-
};
405+
let output = exec_output(&binary.to_string_lossy(), &args, Some(&cwd), timeout)?;
400406
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
401407
if output.status.success() {
402408
return Ok(stdout);
@@ -406,12 +412,14 @@ fn mcp_command(
406412
}
407413

408414
#[tauri::command]
409-
pub async fn claude_mcp_list(cwd: String) -> Result<String, String> {
415+
pub async fn claude_mcp_list(host: State<'_, HarnessHost>, cwd: String) -> Result<String, String> {
416+
let binary_path = host.runtime_binary_path("claude");
410417
tauri::async_runtime::spawn_blocking(move || {
411418
let mut output = claude_mcp_command(
412419
vec!["mcp".into(), "list".into()],
413420
cwd.clone(),
414421
Duration::from_secs(30),
422+
binary_path.as_deref(),
415423
)?;
416424
for name in configured_ws_mcp_servers(&expand_home(&cwd)) {
417425
if !output
@@ -471,6 +479,7 @@ fn configured_ws_mcp_servers(cwd: &Path) -> Vec<String> {
471479

472480
#[tauri::command]
473481
pub async fn claude_mcp_add(
482+
host: State<'_, HarnessHost>,
474483
cwd: String,
475484
name: String,
476485
config: String,
@@ -486,6 +495,7 @@ pub async fn claude_mcp_add(
486495
if !value.is_object() {
487496
return Err("Server configuration must be a JSON object".into());
488497
}
498+
let binary_path = host.runtime_binary_path("claude");
489499
tauri::async_runtime::spawn_blocking(move || {
490500
claude_mcp_command(
491501
vec![
@@ -498,6 +508,7 @@ pub async fn claude_mcp_add(
498508
],
499509
cwd,
500510
Duration::from_secs(30),
511+
binary_path.as_deref(),
501512
)
502513
})
503514
.await
@@ -511,14 +522,15 @@ pub(crate) fn add_mcp_via_cli(
511522
cwd: &str,
512523
name: &str,
513524
config: &serde_json::Value,
525+
binary_path: Option<&str>,
514526
) -> Result<(), String> {
515-
let (binary, args) = mcp_add_args(provider, scope, name, config)?;
527+
let (binary, args) = mcp_add_args(provider, scope, name, config, binary_path)?;
516528
mcp_command(binary, args, cwd.to_owned(), Duration::from_secs(30))?;
517529
Ok(())
518530
}
519531

520-
pub(crate) fn opencode_major_version(cwd: &str) -> Result<u32, String> {
521-
let binary = resolve_opencode().ok_or("OpenCode CLI not found")?;
532+
pub(crate) fn opencode_major_version(cwd: &str, binary_path: Option<&str>) -> Result<u32, String> {
533+
let binary = resolve_mcp_binary("opencode", binary_path)?;
522534
let version = mcp_command(
523535
binary,
524536
vec!["--version".into()],
@@ -543,12 +555,13 @@ fn mcp_add_args(
543555
scope: &str,
544556
name: &str,
545557
config: &serde_json::Value,
558+
binary_path: Option<&str>,
546559
) -> Result<(PathBuf, Vec<String>), String> {
547560
if provider == "claude" {
548561
if !matches!(scope, "local" | "project" | "user") {
549562
return Err("Invalid Claude MCP scope".into());
550563
}
551-
let binary = resolve_claude().ok_or("Claude Code CLI not found")?;
564+
let binary = resolve_mcp_binary("claude", binary_path)?;
552565
let config = serde_json::to_string(config).map_err(|e| e.to_string())?;
553566
return Ok((
554567
binary,
@@ -566,7 +579,7 @@ fn mcp_add_args(
566579
return Err("Codex CLI adds user-scoped servers only".into());
567580
}
568581
let binary = match provider {
569-
"codex" => resolve_codex().ok_or("Codex CLI not found")?,
582+
"codex" => resolve_mcp_binary("codex", binary_path)?,
570583
_ => return Err("Unsupported MCP provider".into()),
571584
};
572585
let object = config
@@ -650,18 +663,25 @@ fn mcp_key_values(
650663
}
651664

652665
#[tauri::command]
653-
pub async fn claude_mcp_remove(cwd: String, name: String, scope: String) -> Result<(), String> {
666+
pub async fn claude_mcp_remove(
667+
host: State<'_, HarnessHost>,
668+
cwd: String,
669+
name: String,
670+
scope: String,
671+
) -> Result<(), String> {
654672
if !valid_mcp_name(&name) {
655673
return Err("Invalid MCP server name".into());
656674
}
657675
if !matches!(scope.as_str(), "local" | "project" | "user") {
658676
return Err("Invalid MCP scope".into());
659677
}
678+
let binary_path = host.runtime_binary_path("claude");
660679
tauri::async_runtime::spawn_blocking(move || {
661680
claude_mcp_command(
662681
vec!["mcp".into(), "remove".into(), name, "--scope".into(), scope],
663682
cwd,
664683
Duration::from_secs(30),
684+
binary_path.as_deref(),
665685
)
666686
})
667687
.await
@@ -670,19 +690,28 @@ pub async fn claude_mcp_remove(cwd: String, name: String, scope: String) -> Resu
670690
}
671691

672692
#[tauri::command]
673-
pub async fn mcp_provider_login(cwd: String, provider: String, name: String) -> Result<(), String> {
674-
if !valid_mcp_name(&name) {
693+
pub async fn mcp_provider_login(
694+
host: State<'_, HarnessHost>,
695+
cwd: String,
696+
provider: String,
697+
name: String,
698+
) -> Result<(), String> {
699+
let valid_name = if provider == "opencode" {
700+
!name.trim().is_empty() && !name.chars().any(char::is_control)
701+
} else {
702+
valid_mcp_name(&name)
703+
};
704+
if !valid_name {
675705
return Err("Invalid MCP server name".into());
676706
}
707+
let binary_path = host.runtime_binary_path(&provider);
677708
tauri::async_runtime::spawn_blocking(move || {
678-
let (binary, args) = match provider.as_str() {
679-
"claude" => (resolve_claude(), vec!["mcp", "login"]),
680-
"codex" => (resolve_codex(), vec!["mcp", "login"]),
681-
"cursor" => (resolve_cursor_agent(), vec!["mcp", "login"]),
682-
"opencode" => (resolve_opencode(), vec!["mcp", "auth"]),
709+
let args = match provider.as_str() {
710+
"claude" | "codex" | "cursor" => vec!["mcp", "login"],
711+
"opencode" => vec!["mcp", "auth"],
683712
_ => return Err("Unsupported MCP provider".into()),
684713
};
685-
let binary = binary.ok_or_else(|| format!("{provider} CLI not found"))?;
714+
let binary = resolve_mcp_binary(&provider, binary_path.as_deref())?;
686715
mcp_command(
687716
binary,
688717
args.into_iter().map(String::from).chain([name]).collect(),
@@ -3238,6 +3267,106 @@ mod tests {
32383267
assert_eq!(initialize_runtime_binary_paths(&runtime, new), old);
32393268
}
32403269

3270+
#[cfg(unix)]
3271+
#[test]
3272+
fn mcp_commands_use_active_configured_binaries() {
3273+
use std::os::unix::fs::PermissionsExt;
3274+
3275+
let root =
3276+
std::env::temp_dir().join(format!("monocode-mcp-binaries-{}", uuid::Uuid::new_v4()));
3277+
std::fs::create_dir_all(&root).unwrap();
3278+
let host = HarnessHost::new();
3279+
let mut paths = HashMap::new();
3280+
for (provider, filename) in [
3281+
("claude", "claude"),
3282+
("codex", "codex"),
3283+
("cursor", "cursor-agent"),
3284+
("opencode", "opencode"),
3285+
] {
3286+
let binary = root.join(filename);
3287+
std::fs::write(
3288+
&binary,
3289+
format!("#!/bin/sh\nif [ \"$1\" = --version ]; then echo '{provider} 2.3.4'; else printf '%s\\n' \"$@\"; fi\n"),
3290+
).unwrap();
3291+
std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)).unwrap();
3292+
paths.insert(provider.to_string(), binary.to_string_lossy().into_owned());
3293+
}
3294+
initialize_runtime_binary_paths(&host.runtime_binary_paths, paths.clone());
3295+
initialize_runtime_binary_paths(&host.runtime_binary_paths, HashMap::new());
3296+
let cwd = root.to_string_lossy().into_owned();
3297+
for (provider, path) in &paths {
3298+
let active = host.runtime_binary_path(provider).unwrap();
3299+
assert_eq!(&active, path);
3300+
let binary = resolve_mcp_binary(provider, Some(&active)).unwrap();
3301+
assert_eq!(binary, PathBuf::from(path));
3302+
assert_eq!(
3303+
mcp_command(
3304+
binary,
3305+
vec!["mcp".into(), "login".into(), "docs".into()],
3306+
cwd.clone(),
3307+
Duration::from_secs(5),
3308+
)
3309+
.unwrap(),
3310+
"mcp\nlogin\ndocs"
3311+
);
3312+
}
3313+
assert_eq!(
3314+
claude_mcp_command(
3315+
vec!["mcp".into(), "list".into()],
3316+
cwd.clone(),
3317+
Duration::from_secs(5),
3318+
paths.get("claude").map(String::as_str),
3319+
)
3320+
.unwrap(),
3321+
"mcp\nlist"
3322+
);
3323+
for provider in ["claude", "codex"] {
3324+
let config = serde_json::json!({"command":"node","args":["docs"]});
3325+
let (binary, args) = mcp_add_args(
3326+
provider,
3327+
"user",
3328+
"docs",
3329+
&config,
3330+
paths.get(provider).map(String::as_str),
3331+
)
3332+
.unwrap();
3333+
assert_eq!(binary, PathBuf::from(&paths[provider]));
3334+
assert_eq!(
3335+
&args[..3],
3336+
[
3337+
"mcp",
3338+
if provider == "claude" {
3339+
"add-json"
3340+
} else {
3341+
"add"
3342+
},
3343+
"docs"
3344+
]
3345+
);
3346+
add_mcp_via_cli(
3347+
provider,
3348+
"user",
3349+
&cwd,
3350+
"docs",
3351+
&config,
3352+
paths.get(provider).map(String::as_str),
3353+
)
3354+
.unwrap();
3355+
}
3356+
assert_eq!(
3357+
opencode_major_version(&cwd, paths.get("opencode").map(String::as_str)),
3358+
Ok(2)
3359+
);
3360+
assert!(resolve_mcp_binary(
3361+
"claude",
3362+
Some(&root.join("missing/claude").to_string_lossy())
3363+
)
3364+
.is_err());
3365+
assert!(resolve_mcp_binary("claude", paths.get("codex").map(String::as_str)).is_err());
3366+
assert!(resolve_mcp_binary("pi", paths.get("claude").map(String::as_str)).is_err());
3367+
std::fs::remove_dir_all(root).unwrap();
3368+
}
3369+
32413370
#[cfg(unix)]
32423371
#[test]
32433372
fn configured_binary_paths_fail_closed_and_stay_exact() {

0 commit comments

Comments
 (0)