@@ -121,6 +121,16 @@ pub struct HarnessHost {
121121}
122122
123123impl 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]
473481pub 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\n if [ \" $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\n login\n docs"
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\n list"
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