@@ -318,6 +318,65 @@ pub fn reconcile_persona_pack_paths(app: &tauri::AppHandle) {
318318 reconcile_pack_paths_in_file ( & path, & canonical_dir) ;
319319}
320320
321+ fn reconcile_mcp_commands_in_file ( path : & Path ) {
322+ patch_json_records ( path, |obj| {
323+ let agent_command = match obj. get ( "agent_command" ) . and_then ( |v| v. as_str ( ) ) {
324+ Some ( cmd) => cmd. to_string ( ) ,
325+ None => return false ,
326+ } ;
327+ let Some ( runtime) = crate :: managed_agents:: known_acp_runtime ( & agent_command) else {
328+ return false ;
329+ } ;
330+ let expected = runtime. mcp_command . unwrap_or ( "" ) ;
331+ let current = obj
332+ . get ( "mcp_command" )
333+ . and_then ( |v| v. as_str ( ) )
334+ . unwrap_or ( "" ) ;
335+ if current == expected {
336+ return false ;
337+ }
338+ // Only fix values that are clearly stale (empty or a removed binary).
339+ // Leave user-customized values untouched.
340+ if !current. is_empty ( ) && current != "sprout-mcp-server" {
341+ return false ;
342+ }
343+ eprintln ! (
344+ "sprout-desktop: runtime-reconcile: {:?} ({:?}): mcp_command {:?} → {:?}" ,
345+ obj. get( "name" ) . and_then( |v| v. as_str( ) ) . unwrap_or( "?" ) ,
346+ agent_command,
347+ current,
348+ expected,
349+ ) ;
350+ obj. insert (
351+ "mcp_command" . to_string ( ) ,
352+ serde_json:: Value :: String ( expected. to_string ( ) ) ,
353+ ) ;
354+ true
355+ } ) ;
356+ }
357+
358+ /// Reconcile `mcp_command` values in managed-agents.json against the
359+ /// discovery table. Known runtimes get their canonical mcp_command;
360+ /// unknown/custom agents are left untouched. Covers both the current
361+ /// app data dir and the canonical dev data dir (for worktree instances).
362+ pub fn reconcile_provider_mcp_commands ( app : & tauri:: AppHandle ) {
363+ let Ok ( current_dir) = app. path ( ) . app_data_dir ( ) else {
364+ return ;
365+ } ;
366+ let mut dirs = vec ! [ current_dir. clone( ) ] ;
367+ if let Some ( canonical) = canonical_dev_data_dir ( & current_dir) {
368+ if canonical. exists ( ) && canonical != current_dir {
369+ dirs. push ( canonical) ;
370+ }
371+ }
372+ for dir in dirs {
373+ let path = dir. join ( "agents/managed-agents.json" ) ;
374+ if path. exists ( ) {
375+ reconcile_mcp_commands_in_file ( & path) ;
376+ }
377+ }
378+ }
379+
321380fn rename_provider_to_runtime_in_personas ( path : & Path ) {
322381 patch_json_records ( path, |obj| {
323382 if obj. contains_key ( "runtime" ) {
@@ -899,4 +958,118 @@ mod tests {
899958 // provider key should still be there since the closure returns false when runtime exists
900959 assert_eq ! ( records[ 0 ] [ "provider" ] , "old-value" ) ;
901960 }
961+
962+ #[ test]
963+ fn reconcile_mcp_commands_clears_stale_sprout_mcp_server ( ) {
964+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
965+ write_agents_json (
966+ dir. path ( ) ,
967+ & serde_json:: json!( [ {
968+ "name" : "Solo" ,
969+ "agent_command" : "goose" ,
970+ "mcp_command" : "sprout-mcp-server"
971+ } ] ) ,
972+ ) ;
973+ reconcile_mcp_commands_in_file ( & dir. path ( ) . join ( "agents/managed-agents.json" ) ) ;
974+ let records = read_agents_json ( dir. path ( ) ) ;
975+ assert_eq ! ( records[ 0 ] [ "mcp_command" ] , "" ) ;
976+ }
977+
978+ #[ test]
979+ fn reconcile_mcp_commands_sets_canonical_for_sprout_agent ( ) {
980+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
981+ write_agents_json (
982+ dir. path ( ) ,
983+ & serde_json:: json!( [ {
984+ "name" : "Stilgar" ,
985+ "agent_command" : "sprout-agent" ,
986+ "mcp_command" : "sprout-mcp-server"
987+ } ] ) ,
988+ ) ;
989+ reconcile_mcp_commands_in_file ( & dir. path ( ) . join ( "agents/managed-agents.json" ) ) ;
990+ let records = read_agents_json ( dir. path ( ) ) ;
991+ assert_eq ! ( records[ 0 ] [ "mcp_command" ] , "sprout-dev-mcp" ) ;
992+ }
993+
994+ #[ test]
995+ fn reconcile_mcp_commands_leaves_custom_value_untouched ( ) {
996+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
997+ let json = serde_json:: json!( [ {
998+ "name" : "Solo" ,
999+ "agent_command" : "goose" ,
1000+ "mcp_command" : "my-custom-mcp"
1001+ } ] ) ;
1002+ write_agents_json ( dir. path ( ) , & json) ;
1003+ let path = dir. path ( ) . join ( "agents/managed-agents.json" ) ;
1004+ let before = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1005+ reconcile_mcp_commands_in_file ( & path) ;
1006+ assert_eq ! ( before, std:: fs:: read_to_string( & path) . unwrap( ) ) ;
1007+ }
1008+
1009+ #[ test]
1010+ fn reconcile_mcp_commands_leaves_unknown_runtime_untouched ( ) {
1011+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1012+ let json = serde_json:: json!( [ {
1013+ "name" : "Custom" ,
1014+ "agent_command" : "my-custom-agent" ,
1015+ "mcp_command" : "sprout-mcp-server"
1016+ } ] ) ;
1017+ write_agents_json ( dir. path ( ) , & json) ;
1018+ let path = dir. path ( ) . join ( "agents/managed-agents.json" ) ;
1019+ let before = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1020+ reconcile_mcp_commands_in_file ( & path) ;
1021+ assert_eq ! ( before, std:: fs:: read_to_string( & path) . unwrap( ) ) ;
1022+ }
1023+
1024+ #[ test]
1025+ fn reconcile_mcp_commands_is_idempotent ( ) {
1026+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1027+ write_agents_json (
1028+ dir. path ( ) ,
1029+ & serde_json:: json!( [ {
1030+ "name" : "Solo" ,
1031+ "agent_command" : "goose" ,
1032+ "mcp_command" : "sprout-mcp-server"
1033+ } ] ) ,
1034+ ) ;
1035+ let path = dir. path ( ) . join ( "agents/managed-agents.json" ) ;
1036+ reconcile_mcp_commands_in_file ( & path) ;
1037+ let after_first = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1038+ reconcile_mcp_commands_in_file ( & path) ;
1039+ assert_eq ! ( after_first, std:: fs:: read_to_string( & path) . unwrap( ) ) ;
1040+ }
1041+
1042+ #[ test]
1043+ fn reconcile_mcp_commands_handles_mixed_agents ( ) {
1044+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1045+ write_agents_json (
1046+ dir. path ( ) ,
1047+ & serde_json:: json!( [
1048+ { "name" : "Stale Goose" , "agent_command" : "goose" , "mcp_command" : "sprout-mcp-server" } ,
1049+ { "name" : "Clean Goose" , "agent_command" : "goose" , "mcp_command" : "" } ,
1050+ { "name" : "Custom Agent" , "agent_command" : "goose" , "mcp_command" : "my-custom-mcp" } ,
1051+ { "name" : "Stale Sprout" , "agent_command" : "sprout-agent" , "mcp_command" : "sprout-mcp-server" }
1052+ ] ) ,
1053+ ) ;
1054+ reconcile_mcp_commands_in_file ( & dir. path ( ) . join ( "agents/managed-agents.json" ) ) ;
1055+ let records = read_agents_json ( dir. path ( ) ) ;
1056+ assert_eq ! ( records[ 0 ] [ "mcp_command" ] , "" ) ;
1057+ assert_eq ! ( records[ 1 ] [ "mcp_command" ] , "" ) ;
1058+ assert_eq ! ( records[ 2 ] [ "mcp_command" ] , "my-custom-mcp" ) ;
1059+ assert_eq ! ( records[ 3 ] [ "mcp_command" ] , "sprout-dev-mcp" ) ;
1060+ }
1061+
1062+ #[ test]
1063+ fn reconcile_mcp_commands_skips_record_without_agent_command ( ) {
1064+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1065+ let json = serde_json:: json!( [ {
1066+ "name" : "No Command" ,
1067+ "mcp_command" : "sprout-mcp-server"
1068+ } ] ) ;
1069+ write_agents_json ( dir. path ( ) , & json) ;
1070+ let path = dir. path ( ) . join ( "agents/managed-agents.json" ) ;
1071+ let before = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1072+ reconcile_mcp_commands_in_file ( & path) ;
1073+ assert_eq ! ( before, std:: fs:: read_to_string( & path) . unwrap( ) ) ;
1074+ }
9021075}
0 commit comments