Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion app/src/ai/mcp/templatable_manager/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions app/src/ai/mcp/templatable_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Loading