Skip to content

Commit cc50654

Browse files
committed
feat(servers): smarter OAuth startup and sticky My Servers toolbar
Auto-connect OAuth servers on gateway boot when mux already has tokens (stdio OAuth always attempts connect; HTTP only prompts Connect when unauthenticated). Bootstrap oauth_connected after credential-based connect. Pin the My Servers search/actions header while scrolling the server list. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 6b70c0f commit cc50654

3 files changed

Lines changed: 103 additions & 15 deletions

File tree

apps/desktop/src-tauri/src/commands/gateway.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,11 +1575,12 @@ pub async fn connect_all_enabled_servers(
15751575
errors: vec![],
15761576
};
15771577

1578-
for (server_info, transport, _server_definition, _installed) in servers_to_connect {
1578+
for (server_info, transport, _server_definition, installed) in servers_to_connect {
15791579
let space_uuid = server_info.space_id;
15801580
let server_id = server_info.server_id.clone();
15811581

1582-
let ctx = ConnectionContext::new(space_uuid, server_id.clone(), transport);
1582+
let ctx = ConnectionContext::new(space_uuid, server_id.clone(), transport)
1583+
.with_auto_reconnect(true);
15831584
match pool_service.connect_server(&ctx).await {
15841585
ConnectionResult::Connected { reused, features } => {
15851586
if reused {
@@ -1588,6 +1589,19 @@ pub async fn connect_all_enabled_servers(
15881589
result.connected += 1;
15891590
}
15901591

1592+
if server_info.requires_oauth && !installed.oauth_connected {
1593+
if let Err(e) = app_state
1594+
.installed_server_repository
1595+
.set_oauth_connected(&installed.id, true)
1596+
.await
1597+
{
1598+
warn!(
1599+
"[Gateway] Connected {} but failed to set oauth_connected: {}",
1600+
server_id, e
1601+
);
1602+
}
1603+
}
1604+
15911605
info!(
15921606
"[Gateway] Connected {} (reused: {}, features: {})",
15931607
server_id,

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,7 @@ export function ServersPage() {
11351135
}
11361136

11371137
return (
1138-
<div className="space-y-6" data-testid="servers-page">
1138+
<div data-testid="servers-page">
11391139
{gatewayControl.ConfirmDialogElement}
11401140
{uninstallClonesDialog && (
11411141
<UninstallSourceWithClonesDialog
@@ -1147,8 +1147,11 @@ export function ServersPage() {
11471147
onUninstallAll={handleUninstallAllWithClones}
11481148
/>
11491149
)}
1150-
{/* Header */}
1151-
<div className="space-y-4">
1150+
{/* Toolbar — stays visible while the server list scrolls in <main> */}
1151+
<div
1152+
className="sticky -top-6 z-20 -mx-6 mb-4 space-y-4 border-b border-[rgb(var(--border-subtle))] bg-[rgb(var(--background))] px-6 pt-6 pb-4"
1153+
data-testid="servers-page-toolbar"
1154+
>
11521155
<div className="flex items-center justify-between gap-4">
11531156
<div className="flex-shrink min-w-0">
11541157
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">

crates/mcpmux-gateway/src/server/startup.rs

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use std::sync::Arc;
77

88
use anyhow::Result;
9+
use mcpmux_core::domain::{AuthConfig, CredentialType, ServerDefinition, TransportConfig};
910
use mcpmux_core::InstalledServer;
1011
use tracing::{info, warn};
1112

@@ -230,16 +231,17 @@ impl StartupOrchestrator {
230231
let space_id = uuid::Uuid::parse_str(&server.space_id)
231232
.map_err(|e| anyhow::anyhow!("Invalid space_id: {}", e))?;
232233

233-
// Check if server requires OAuth but hasn't been approved yet
234-
// This prevents auto-connect from setting "Connected" status without user approval
235-
let requires_oauth = matches!(
236-
definition.auth,
237-
Some(mcpmux_core::domain::AuthConfig::Oauth)
238-
);
234+
let requires_oauth = matches!(definition.auth, Some(AuthConfig::Oauth));
239235

240-
if requires_oauth && !server.oauth_connected {
236+
if should_skip_oauth_autoconnect(
237+
requires_oauth,
238+
server.oauth_connected,
239+
is_stdio_transport(&definition),
240+
self.has_mux_oauth_credentials(space_id, &server.server_id)
241+
.await?,
242+
) {
241243
info!(
242-
"[Startup] Skipping {}/{} - requires OAuth approval",
244+
"[Startup] Skipping {}/{} - HTTP OAuth with no stored credentials and no prior approval",
243245
server.space_id, server.server_id
244246
);
245247
let key = crate::pool::ServerKey::new(space_id, server.server_id.clone());
@@ -270,10 +272,27 @@ impl StartupOrchestrator {
270272

271273
match connection_result {
272274
ConnectionResult::Connected { reused, features } => {
273-
// Explicitly update ServerManager status to Connected
274-
// While PoolService might update instance state, ServerManager is the source of truth for UI events
275275
self.server_manager.set_connected(&key, features).await;
276276

277+
if requires_oauth && !server.oauth_connected {
278+
if let Err(e) = self
279+
.dependencies
280+
.installed_server_repo
281+
.set_oauth_connected(&server.id, true)
282+
.await
283+
{
284+
warn!(
285+
"[Startup] Connected {}/{} but failed to set oauth_connected: {}",
286+
server.space_id, server.server_id, e
287+
);
288+
} else {
289+
info!(
290+
"[Startup] Bootstrapped oauth_connected for {}/{} after credential-based connect",
291+
server.space_id, server.server_id
292+
);
293+
}
294+
}
295+
277296
if reused {
278297
Ok(ConnectOutcome::AlreadyConnected)
279298
} else {
@@ -317,3 +336,55 @@ enum ConnectOutcome {
317336
AlreadyConnected,
318337
NeedsOAuth,
319338
}
339+
340+
impl StartupOrchestrator {
341+
/// Whether mux has a stored OAuth access token for this install.
342+
async fn has_mux_oauth_credentials(
343+
&self,
344+
space_id: uuid::Uuid,
345+
server_id: &str,
346+
) -> Result<bool> {
347+
Ok(self
348+
.dependencies
349+
.credential_repo
350+
.get(&space_id, server_id, &CredentialType::AccessToken)
351+
.await?
352+
.is_some())
353+
}
354+
}
355+
356+
/// Stdio MCPs manage auth inside the child process; do not gate on `oauth_connected`.
357+
fn is_stdio_transport(definition: &ServerDefinition) -> bool {
358+
matches!(definition.transport, TransportConfig::Stdio { .. })
359+
}
360+
361+
/// Skip auto-connect and show Connect Required only for HTTP OAuth with no mux tokens
362+
/// and no prior user approval (`oauth_connected`).
363+
fn should_skip_oauth_autoconnect(
364+
requires_oauth: bool,
365+
oauth_connected: bool,
366+
is_stdio: bool,
367+
has_mux_credentials: bool,
368+
) -> bool {
369+
if !requires_oauth {
370+
return false;
371+
}
372+
if is_stdio {
373+
return false;
374+
}
375+
!oauth_connected && !has_mux_credentials
376+
}
377+
378+
#[cfg(test)]
379+
mod tests {
380+
use super::*;
381+
382+
#[test]
383+
fn skip_only_http_oauth_without_credentials_or_approval() {
384+
assert!(!should_skip_oauth_autoconnect(false, false, false, false));
385+
assert!(!should_skip_oauth_autoconnect(true, false, true, false));
386+
assert!(!should_skip_oauth_autoconnect(true, true, false, false));
387+
assert!(!should_skip_oauth_autoconnect(true, false, false, true));
388+
assert!(should_skip_oauth_autoconnect(true, false, false, false));
389+
}
390+
}

0 commit comments

Comments
 (0)