From 1f3f202aa4ad78f75bd40126a9a79cdbfdb2721f Mon Sep 17 00:00:00 2001 From: luigi-facilgroup Date: Tue, 27 Oct 2026 16:55:09 -0600 Subject: [PATCH] Don't retry the built-in Factory MCP spawn with a token that already failed When the built-in Factory MCP server fails to start (e.g. a 403 from the server), every subsequent auth event (token refresh, user refresh) respawned it with the same bearer token, repeating the same failure indefinitely. Treat a failed spawn for the current token like a live one so we only retry once the token rotates. Co-Authored-By: Claude Opus 5.5 --- app/src/ai/mcp/templatable_manager/native.rs | 18 ++++++++++++++++- app/src/ai/mcp/templatable_manager_tests.rs | 21 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/src/ai/mcp/templatable_manager/native.rs b/app/src/ai/mcp/templatable_manager/native.rs index ffbd03c2bdd..030b169be6f 100644 --- a/app/src/ai/mcp/templatable_manager/native.rs +++ b/app/src/ai/mcp/templatable_manager/native.rs @@ -851,7 +851,7 @@ impl TemplatableMCPServerManager { // token refresh) and usually carry the same credential. Respawning // for each would open a redundant server-side MCP session per event, // so only respawn when the effective bearer actually changed. - if is_active && self.builtin_server_token.as_deref() == Some(token.as_str()) { + if self.builtin_spawn_settled_for_token(installation_uuid, &token) { return; } @@ -867,6 +867,22 @@ impl TemplatableMCPServerManager { self.spawn_ephemeral_server(builtin::factory_mcp_installation(&token, &[]), ctx); } + /// Whether the built-in server's last spawn used `token` and is still live, pending, or failed. + /// A failed spawn counts because retrying with the same credential (e.g. after a 403) would + /// fail again on every auth event; a rotated token gets a fresh attempt. + pub(super) fn builtin_spawn_settled_for_token( + &self, + installation_uuid: Uuid, + token: &str, + ) -> bool { + self.builtin_server_token.as_deref() == Some(token) + && (self.is_server_active_or_pending(installation_uuid) + || matches!( + self.server_states.get(&installation_uuid), + Some(MCPServerState::FailedToStart) + )) + } + /// Spawns a new MCP server from a given installation UUID. /// /// This looks up the installation from `locally_installed_servers` and persists diff --git a/app/src/ai/mcp/templatable_manager_tests.rs b/app/src/ai/mcp/templatable_manager_tests.rs index f0732d8a7c4..4155b5d8242 100644 --- a/app/src/ai/mcp/templatable_manager_tests.rs +++ b/app/src/ai/mcp/templatable_manager_tests.rs @@ -265,3 +265,24 @@ fn shutdown_ends_ephemeral_reconnect_lifecycle() { assert_eq!(error, "Server shut down"); }); } + +#[test] +fn builtin_spawn_with_failed_token_is_not_retried_until_token_rotates() { + use crate::ai::mcp::MCPServerState; + + let installation_uuid = builtin::FACTORY_MCP_INSTALLATION_UUID; + let mut manager = TemplatableMCPServerManager::default(); + assert!(!manager.builtin_spawn_settled_for_token(installation_uuid, "token")); + + manager.builtin_server_token = Some("token".into()); + manager + .server_states + .insert(installation_uuid, MCPServerState::FailedToStart); + assert!(manager.builtin_spawn_settled_for_token(installation_uuid, "token")); + assert!(!manager.builtin_spawn_settled_for_token(installation_uuid, "rotated-token")); + + manager + .server_states + .insert(installation_uuid, MCPServerState::NotRunning); + assert!(!manager.builtin_spawn_settled_for_token(installation_uuid, "token")); +}