Skip to content

Commit 02cc882

Browse files
wesbillmanBrainDuncan
committed
fix(desktop): reconcile stale mcp_command and harden spawn site
Re-enable the generic `reconcile_provider_mcp_commands` migration that was accidentally removed from the startup sequence in PR #877. This reconciles `mcp_command` values in managed-agents.json against the discovery table on every launch — fixing stale "sprout-mcp-server" references and any future drift without a dedicated one-off function. Extended to cover both `app_data_dir()` and `canonical_dev_data_dir` so worktree instances are also healed. Additionally, harden the spawn site in runtime.rs: if `mcp_command` references a binary that cannot be resolved, log a warning and continue spawning without MCP rather than hard-failing. This prevents this entire class of breakage permanently, regardless of whether reconciliation ran. Fixes the user-reported issue where agents created before v0.3.12 fail to spawn because "sprout-mcp-server" no longer exists. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
1 parent dd08f98 commit 02cc882

3 files changed

Lines changed: 188 additions & 8 deletions

File tree

desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ pub fn run() {
499499
// restore_managed_agents_on_launch (which reads managed-agents.json).
500500
migration::sync_shared_agent_data(&app_handle);
501501
migration::reconcile_persona_pack_paths(&app_handle);
502+
migration::reconcile_provider_mcp_commands(&app_handle);
502503
migration::migrate_persona_provider_to_runtime(&app_handle);
503504

504505
// Resolve persisted identity key (env var → file → generate+save).

desktop/src-tauri/src/managed_agents/runtime.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -797,14 +797,20 @@ pub fn spawn_agent_child(
797797
let agent_args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
798798
let resolved_acp_command = resolve_command(&record.acp_command)
799799
.ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
800-
let resolved_mcp_command: Option<std::path::PathBuf> =
801-
if record.mcp_command.is_empty() {
802-
None
803-
} else {
804-
Some(resolve_command(&record.mcp_command).ok_or_else(|| {
805-
missing_command_message(&record.mcp_command, "MCP server command")
806-
})?)
807-
};
800+
let resolved_mcp_command: Option<std::path::PathBuf> = if record.mcp_command.is_empty() {
801+
None
802+
} else {
803+
match resolve_command(&record.mcp_command) {
804+
Some(path) => Some(path),
805+
None => {
806+
eprintln!(
807+
"sprout-desktop: mcp_command {:?} not found, skipping",
808+
record.mcp_command
809+
);
810+
None
811+
}
812+
}
813+
};
808814
// Resolve agent command to a full path (DMG launches have minimal PATH).
809815
let resolved_agent_command = resolve_command(&record.agent_command)
810816
.map(|p| p.display().to_string())

desktop/src-tauri/src/migration.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
321380
fn 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

Comments
 (0)