Skip to content

Commit 5678086

Browse files
Dumbrisclaude
andauthored
fix: skip browser OAuth flow when valid token exists in storage (#181)
* fix: skip browser OAuth flow when valid token exists in storage When OAuth was completed via CLI or another process, the daemon would still trigger browser OAuth flow even though a valid token existed in persistent storage. This caused unnecessary OAuth prompts and confusion. Root cause: - `HasRecentOAuthCompletion()` only checks in-memory state (per-process) - Cross-process OAuth completion was not detected - Browser flow was triggered even when valid token existed Fix: - Add direct check for valid persisted token before OAuth flow - Set `skipBrowserFlow` flag when valid token found - Skip browser OAuth and return retriable error when flag is set - Applied to both HTTP and SSE OAuth flows - Applied to both initial connection and MCP init OAuth errors Test results: - Core unit tests pass - OAuth unit tests pass - OAuth E2E tests pass (29 tests) Closes: docs/bugs/oauth-reconnection-after-login.md 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add 'Connecting' state notification for better UI feedback - Add NotifyServerConnecting helper method - Add StateConnecting case to StateChangeNotifier - UI now shows "Connecting" badge immediately when reconnection starts - Triggers SSE event for real-time UI updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: support logout for servers with discovered OAuth Previously, ClearOAuthToken() required serverConfig.OAuth != nil, which failed for servers using discovered OAuth (server-announced OAuth). This caused a 500 error when clicking "Logout" in the Web UI for servers like Sentry MCP that use OAuth discovery. The fix removes the OAuth config requirement check, allowing logout to work for both explicit OAuth config and discovered OAuth servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: prevent showing Login and Logout buttons simultaneously If a server is authenticated, only show Logout button. Login button should only appear for non-authenticated servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add connecting status to API response The backend now returns a 'connecting' field in the server API response, enabling the Web UI and CLI to show "Connecting" status when a server is in the process of connecting (e.g., during OAuth flow or reconnection). Changes: - contracts/types.go: Add Connecting bool field to Server struct - contracts/converters.go: Extract connecting field in converter - management/service.go: Extract connecting field in ListServers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use actual server name for proactive token refresh The proactive token refresh was failing because RefreshManager was using the storage key format (serverName_hash) instead of the actual server name when looking up servers for refresh. Changes: - Add DisplayName field to OAuthTokenRecord to store actual server name - Add GetServerName() helper that returns DisplayName if set, falls back to ServerName for backward compatibility - Update PersistentTokenStore to store both serverKey and serverName - Update RefreshManager to use GetServerName() when loading tokens This fixes the "server not found" error during proactive token refresh. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show Login button immediately after Logout in Web UI Add optimistic update to triggerOAuthLogout to immediately set authenticated = false, ensuring the Login button appears right away instead of waiting for SSE event refresh. The issue was that clicking Logout would clear the token but the UI wouldn't update immediately because it was waiting for the servers.changed SSE event to trigger a refresh. Now the authenticated field is cleared optimistically with proper rollback on error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve OAuth UX with oauth_status for accurate state display Use oauth_status field for more accurate OAuth state representation: - Frontend: Update needsOAuth/canLogout logic to use oauth_status - Login shows when: disconnected + token expired/error/none - Logout shows when: connected OR has error with valid token - Hidden states: Login during connecting, Logout when token expired - Backend: Add oauth_status and token_expires_at to server response - oauth_status: "authenticated" | "expired" | "error" | "none" - Calculated from stored token validity in runtime.go - CLI: Update auth status to show clearer messages - "Reconnecting (Token Valid)" instead of "Authenticated (Disconnected)" - "Token Expired (Login Required)" for expired tokens - API: Add connecting field to OpenAPI Server schema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: prevent auto-reconnection after explicit OAuth logout When a user clicks Logout, the server should stay disconnected until the user explicitly logs in again. Previously, the exponential backoff reconnection logic would auto-reconnect the server shortly after logout. This fix introduces a `userLoggedOut` flag in the StateManager that: - Gets set to true when TriggerOAuthLogout is called (before other operations) - Prevents ShouldRetry() from returning true while the flag is set - Gets cleared on StateReady transition (successful connection) - Gets cleared when TriggerOAuthLogin is called (explicit login) The order of operations in TriggerOAuthLogout is critical to prevent race conditions: the flag must be set FIRST before ClearOAuthToken or DisconnectServer, otherwise reconnection could trigger before the flag is set. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: make supervisor respect userLoggedOut flag to prevent reconnection The previous fix (7aaba6a) added the userLoggedOut flag to StateManager and prevented ShouldRetry() from returning true after explicit logout. However, the supervisor's reconcile loop was operating at a higher level and still reconnecting servers by computing ActionConnect for any enabled server that wasn't connected. This fix adds IsUserLoggedOut() to the UpstreamInterface and updates computeReconcilePlan() to check this flag before deciding to reconnect. Now when a user clicks Logout: 1. StateManager.userLoggedOut flag is set (prevents retry logic) 2. Supervisor sees the server is disconnected but checks IsUserLoggedOut() 3. If true, supervisor sets ActionNone instead of ActionConnect 4. Server stays disconnected until user explicitly logs in again 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve OAuth UX with user_logged_out flag and button visibility - Add user_logged_out field to API response and contracts for tracking explicit user logout action - Update supervisor to respect userLoggedOut flag and prevent auto-reconnection after explicit logout - Fix ServerCard.vue button visibility to prevent Login and Logout buttons from appearing simultaneously when OAuth error present with stored token - Update CLI auth status to show disabled/logged-out state correctly - Add OAuth manager tests for token persistence and logout flow - Update OpenAPI spec with user_logged_out field documentation The user_logged_out flag captures user intent to logout separately from connection state and config state, enabling proper UX where: - After logout: Login button visible, no auto-reconnection - Connected: Logout button visible - OAuth error with expired token: Login button visible (not Logout) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: update golden file for API contract compliance Add 'connecting' field to get_servers.json golden file to match the actual API response. The 'user_logged_out' field is not included because it uses omitempty and defaults to false. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 47d87b3 commit 5678086

29 files changed

Lines changed: 704 additions & 157 deletions

cmd/mcpproxy/auth_cmd.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ func runAuthStatusClientMode(ctx context.Context, dataDir, serverName string, al
230230
authenticated, _ := srv["authenticated"].(bool)
231231
connected, _ := srv["connected"].(bool)
232232
lastError, _ := srv["last_error"].(string)
233+
enabled, _ := srv["enabled"].(bool)
234+
userLoggedOut, _ := srv["user_logged_out"].(bool)
233235

234236
// Check if this is an OAuth server by:
235237
// 1. Has oauth config OR
@@ -245,18 +247,46 @@ func runAuthStatusClientMode(ctx context.Context, dataDir, serverName string, al
245247

246248
hasOAuthServers = true
247249

248-
// Determine status emoji and text (improved logic)
250+
// Determine status emoji and text using oauth_status for accurate state
251+
oauthStatus, _ := srv["oauth_status"].(string)
249252
var status string
250-
if authenticated && connected {
251-
status = "✅ Authenticated & Connected"
252-
} else if authenticated && !connected {
253-
status = "✅ Authenticated (Disconnected)"
253+
254+
// Check priority states first
255+
if !enabled {
256+
// Server is disabled - no reconnection attempts
257+
status = "⏸️ Disabled"
258+
} else if userLoggedOut {
259+
// User explicitly logged out - no auto-reconnection
260+
status = "🚪 Logged Out (Login Required)"
254261
} else if connected {
255-
status = "⚠️ Connected (No OAuth Token)"
256-
} else if lastError != "" {
257-
status = "❌ Authentication Failed"
262+
// Connected states
263+
if authenticated {
264+
status = "✅ Authenticated & Connected"
265+
} else {
266+
status = "⚠️ Connected (No OAuth Token)"
267+
}
258268
} else {
259-
status = "⏳ Pending Authentication"
269+
// Disconnected states - use oauth_status for clarity
270+
switch oauthStatus {
271+
case "authenticated":
272+
// Token valid but not connected - likely reconnecting
273+
status = "⏳ Reconnecting (Token Valid)"
274+
case "expired":
275+
// Token expired - needs re-authentication
276+
status = "⚠️ Token Expired (Login Required)"
277+
case "error":
278+
status = "❌ Authentication Error"
279+
default:
280+
// No token or oauth_status not set
281+
if lastError != "" {
282+
status = "❌ Authentication Failed"
283+
} else if authenticated {
284+
// Fallback: has token but no oauth_status
285+
status = "⏳ Reconnecting"
286+
} else {
287+
status = "⏳ Pending Authentication"
288+
}
289+
}
260290
}
261291

262292
fmt.Printf("Server: %s\n", name)

docs/bugs/oauth-reconnection-after-login.md

Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Bug: Server doesn't connect immediately after OAuth login
22

3+
## Status: FIXED
4+
5+
**Fixed in:** PR #181 (fix/oauth-reconnection-after-login branch)
6+
**Fix Date:** 2025-12-07
7+
38
## Summary
49
After successful OAuth authentication via browser flow, the server remains in "Disconnected" status instead of automatically connecting. Users must manually restart the server to establish a connection.
510

@@ -9,82 +14,113 @@ After OAuth login completes successfully:
914
2. Server automatically attempts connection using the new token
1015
3. Server status updates to "Connected" in UI
1116

12-
## Actual Behavior
17+
## Actual Behavior (BEFORE FIX)
1318
After OAuth login completes:
1419
1. Token is saved to persistent storage ✅
1520
2. `RetryConnection` is called but fails with "OAuth authentication required"
1621
3. Server remains "Disconnected" despite having a valid token
1722

1823
## Root Cause Analysis
1924

20-
The issue is in `internal/upstream/core/connection.go` lines 1028-1033:
21-
22-
```go
23-
// Check if we have a recently completed OAuth flow that should have tokens
24-
if oauth.GetTokenStoreManager().HasRecentOAuthCompletion(c.serverName) {
25-
c.logger.Info("OAuth flow recently completed, tokens should be available",
26-
zap.String("server", c.serverName))
27-
// Skip the browser flow since tokens should be available
28-
}
29-
```
30-
31-
**Problem**: The code logs "Skip the browser flow since tokens should be available" but doesn't actually change the control flow. The browser OAuth flow is still triggered, which fails because OAuth is already complete.
25+
The issue was in `internal/upstream/core/connection.go`:
3226

33-
### Why this happens:
27+
1. **Cross-process OAuth issue**: When CLI completed OAuth, the daemon's `HasRecentOAuthCompletion()` check returned false because it only checks in-memory state (per-process)
28+
2. **Missing token pre-check**: The code didn't directly check if a valid persisted token existed before triggering browser OAuth flow
29+
3. **No flow skip logic**: Even when a valid token was detected, the code only logged the finding but didn't actually skip the browser OAuth flow
3430

35-
1. **Managed client calls `RetryConnection`** (`internal/upstream/manager.go:1326-1355`)
36-
2. **Core client's `Connect()` is called** which goes through `connectWithOAuth()`
37-
3. **`connectWithOAuth()` checks `HasRecentOAuthCompletion()`** but only logs - doesn't skip OAuth
38-
4. **OAuth flow coordinator returns "flow already active" or times out**
39-
5. **Connection fails** even though valid token exists in storage
31+
### Why this happened:
4032

41-
### The architectural issue:
33+
1. **Managed client calls `RetryConnection`** (`internal/upstream/manager.go`)
34+
2. **Core client's `Connect()` is called** which goes through `tryOAuthAuth()`
35+
3. **`tryOAuthAuth()` checks `HasRecentOAuthCompletion()`** - but this is per-process, so cross-process OAuth (CLI → daemon) wasn't detected
36+
4. **OAuth flow coordinator triggers browser flow** even though valid token exists in storage
37+
5. **Connection fails** unnecessarily
4238

43-
The core client (`internal/upstream/core/`) doesn't directly read tokens from the persistent token store during connection. Instead:
44-
- Tokens are managed by `PersistentTokenStore` in `internal/oauth/persistent_token_store.go`
45-
- Core client relies on OAuth flow to provide tokens
46-
- When OAuth flow is skipped/already complete, no mechanism exists to retrieve existing tokens
39+
## The Fix
4740

48-
## Proposed Fix
41+
The fix adds three key improvements to `internal/upstream/core/connection.go`:
4942

50-
Option A: **Skip OAuth flow when recent completion detected**
43+
### 1. Direct Persisted Token Check (lines ~1032-1045)
5144
```go
52-
if oauth.GetTokenStoreManager().HasRecentOAuthCompletion(c.serverName) {
53-
c.logger.Info("OAuth flow recently completed, using existing token")
54-
// Actually skip the OAuth flow - just attempt connection with existing token
55-
return c.connectWithExistingToken(ctx)
45+
// First check for valid persisted token directly (handles cross-process OAuth)
46+
hasTokenPrecheck, hasRefreshPrecheck, tokenExpiredPrecheck := oauth.HasPersistedToken(c.config.Name, c.config.URL, c.storage)
47+
if hasTokenPrecheck && !tokenExpiredPrecheck {
48+
logger.Info("🔄 Valid OAuth token found in persistent storage - will skip browser flow if OAuth error occurs",
49+
zap.String("server", c.config.Name),
50+
zap.Bool("has_refresh_token", hasRefreshPrecheck))
51+
skipBrowserFlow = true
52+
} else if tokenManager.HasRecentOAuthCompletion(c.config.Name) {
53+
// Also check in-memory completion flag (same-process OAuth)
54+
...
5655
}
5756
```
5857

59-
Option B: **Add token retrieval to core client**
60-
- Add method to retrieve token from persistent store
61-
- Use token directly in HTTP transport without triggering OAuth flow
58+
### 2. Skip Browser Flow When Token Exists (lines ~1217-1230)
59+
```go
60+
if client.IsOAuthAuthorizationRequiredError(lastErr) {
61+
// CRITICAL FIX: If we have a valid persisted token (e.g., from CLI OAuth),
62+
// skip browser flow and return retriable error. The token exists but
63+
// the mcp-go client needs to pick it up on retry.
64+
if skipBrowserFlow {
65+
c.logger.Info("🔄 OAuth authorization error but valid token exists - skipping browser flow",
66+
zap.String("server", c.config.Name),
67+
zap.String("tip", "Token should be used on next connection attempt"))
68+
69+
c.clearOAuthState()
70+
return fmt.Errorf("OAuth token exists in storage, retry connection to use it: %w", lastErr)
71+
}
72+
// ... proceed with browser flow only if no valid token exists
73+
}
74+
```
6275

63-
Option C: **Fix RetryConnection to reinitialize transport with token**
64-
- When `RetryConnection` is called after OAuth completion
65-
- Reinitialize the HTTP transport with the token from storage
66-
- Skip the full OAuth flow
76+
### 3. Same Fix Applied to SSE OAuth Flow (lines ~1465-1683)
77+
The same `skipBrowserFlow` logic was applied to the SSE OAuth flow to handle SSE-based OAuth servers.
6778

6879
## Affected Files
6980
- `internal/upstream/core/connection.go` - Main fix location
70-
- `internal/upstream/core/client.go` - May need token retrieval method
71-
- `internal/upstream/manager.go` - RetryConnection logic
81+
- HTTP OAuth flow: lines ~1026-1350
82+
- SSE OAuth flow: lines ~1465-1710
7283

73-
## Reproduction Steps
84+
## Test Results
85+
- ✅ Core unit tests pass
86+
- ✅ OAuth unit tests pass
87+
- ✅ OAuth E2E tests pass (29 tests)
88+
- ✅ API E2E tests pass (except 1 unrelated test)
89+
90+
## Reproduction Steps (BEFORE FIX)
7491
1. Configure an OAuth-enabled server (e.g., cloudflare-logs)
7592
2. Start mcpproxy - server shows as "Disconnected"
7693
3. Click "Login" button or run `mcpproxy auth login --server=<name>`
7794
4. Complete OAuth flow in browser
7895
5. Observe: Server still shows "Disconnected" despite successful login
7996
6. Click "Restart" to manually connect
8097

81-
## Impact
82-
- User experience degradation - requires manual intervention after OAuth
83-
- Confusion about whether OAuth succeeded
84-
- Extra clicks/commands to get server connected
98+
## Expected Behavior (AFTER FIX)
99+
1. Configure an OAuth-enabled server
100+
2. Start mcpproxy - server shows as "Disconnected"
101+
3. Complete OAuth login via CLI or Web UI
102+
4. Server automatically retries connection with existing token
103+
5. Server status updates to "Connected" (or returns retriable error for managed client to retry)
85104

86-
## Priority
87-
Medium - Workaround exists (manual restart), but UX is poor
105+
## Additional Improvements
106+
107+
### 4. "Connecting" State Notification (notifications.go)
108+
Added `StateConnecting` to the state change notifier so UI shows "Connecting" immediately when reconnection starts:
109+
```go
110+
case types.StateConnecting:
111+
// Notify when connection attempt starts (important for UI feedback after OAuth)
112+
if oldState != types.StateConnecting {
113+
nm.NotifyServerConnecting(serverName)
114+
}
115+
```
116+
117+
This triggers an SSE event to refresh the UI, showing "Connecting" badge instead of "Disconnected" during reconnection.
118+
119+
## Impact
120+
- ✅ Better user experience - no manual intervention needed after OAuth
121+
- ✅ Clear feedback about OAuth status ("Connecting" badge during reconnection)
122+
- ✅ Reduced confusion about whether OAuth succeeded
123+
- ✅ Real-time UI updates via SSE when connection state changes
88124

89125
## Related
90126
- Feature 009: Proactive OAuth Token Refresh (this bug was discovered during testing)

frontend/src/components/ServerCard.vue

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,41 @@ const loading = ref(false)
174174
const showDeleteConfirmation = ref(false)
175175
176176
const needsOAuth = computed(() => {
177-
// Check if server requires OAuth authentication
177+
// Don't show Login button if server is disabled
178+
if (!props.server.enabled) return false
179+
180+
// Don't show Login button while connecting (let the connection attempt finish)
181+
if (props.server.connecting) return false
182+
178183
const isHttpProtocol = props.server.protocol === 'http' || props.server.protocol === 'streamable-http'
179-
const notConnected = !props.server.connected
180-
const isEnabled = props.server.enabled
184+
if (!isHttpProtocol) return false
185+
186+
// Don't show Login if already connected
187+
if (props.server.connected) return false
188+
189+
// Check if server has OAuth configuration
190+
const hasOAuthConfig = props.server.oauth !== null && props.server.oauth !== undefined
191+
if (!hasOAuthConfig) return false
192+
193+
// Show Login if user explicitly logged out
194+
if (props.server.user_logged_out) {
195+
return true
196+
}
197+
198+
// Use oauth_status for more accurate token state detection
199+
// Show Login if: no token, expired token, or error state
200+
const oauthStatus = props.server.oauth_status
201+
if (oauthStatus === 'expired' || oauthStatus === 'error' || oauthStatus === 'none') {
202+
return true
203+
}
181204
182-
// Check for OAuth-related errors in last_error
205+
// Fallback: if oauth_status not available, use authenticated flag
206+
// Show Login if not authenticated (no stored token)
207+
if (!props.server.authenticated) {
208+
return true
209+
}
210+
211+
// Check for OAuth-related errors that indicate re-authentication is needed
183212
const hasOAuthError = props.server.last_error && (
184213
props.server.last_error.includes('authorization') ||
185214
props.server.last_error.includes('OAuth') ||
@@ -188,21 +217,58 @@ const needsOAuth = computed(() => {
188217
props.server.last_error.includes('Missing or invalid access token')
189218
)
190219
191-
// Check if server has OAuth configuration
192-
const hasOAuthConfig = props.server.oauth !== null && props.server.oauth !== undefined
193-
194-
// Check if server is authenticated
195-
const notAuthenticated = !props.server.authenticated
196-
197-
return isHttpProtocol && notConnected && isEnabled && (hasOAuthError || (hasOAuthConfig && notAuthenticated))
220+
return hasOAuthError
198221
})
199222
200223
const canLogout = computed(() => {
201-
// Show logout button if server is authenticated (has OAuth token)
224+
// Don't show Logout button if server is disabled
225+
if (!props.server.enabled) return false
226+
227+
// Don't show Logout if user already explicitly logged out
228+
if (props.server.user_logged_out) return false
229+
202230
const isHttpProtocol = props.server.protocol === 'http' || props.server.protocol === 'streamable-http'
203-
const isAuthenticated = props.server.authenticated === true
231+
if (!isHttpProtocol) return false
232+
233+
const hasToken = props.server.authenticated === true
234+
if (!hasToken) return false
235+
236+
// Show Logout when:
237+
// 1. Connected with valid token (normal case)
238+
// 2. Has error but token is still valid (user may want to clear token to re-authenticate)
239+
//
240+
// Don't show Logout when:
241+
// - Disconnected without error and token expired (show Login instead)
242+
// - Server is connecting (wait for connection to complete)
243+
244+
if (props.server.connecting) return false
245+
246+
// If connected, always show Logout (user can log out of working connection)
247+
if (props.server.connected) return true
248+
249+
// If not connected but has error, check if it's an OAuth authentication error
250+
// If OAuth auth is required, show Login instead of Logout
251+
if (props.server.last_error) {
252+
// Don't show Logout if oauth_status says token is expired
253+
// In that case, Login is more appropriate
254+
if (props.server.oauth_status === 'expired') return false
255+
256+
// Don't show Logout if the error indicates OAuth authentication is required
257+
// This means the stored token isn't valid, so Login is more appropriate
258+
const isOAuthRequired = props.server.last_error.includes('OAuth authentication required') ||
259+
props.server.last_error.includes('authorization') ||
260+
props.server.last_error.includes('401') ||
261+
props.server.last_error.includes('invalid_token')
262+
if (isOAuthRequired) return false
263+
264+
return true
265+
}
266+
267+
// Not connected, no error, has token - mcpproxy is likely trying to reconnect
268+
// Show Logout only if token is still valid (authenticated status)
269+
if (props.server.oauth_status === 'authenticated') return true
204270
205-
return isHttpProtocol && isAuthenticated
271+
return false
206272
})
207273
208274
async function toggleEnabled() {

frontend/src/stores/servers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,32 @@ export const useServersStore = defineStore('servers', () => {
207207

208208
async function triggerOAuthLogout(serverName: string) {
209209
try {
210+
const server = servers.value.find(s => s.name === serverName)
211+
212+
// Optimistic update: clear authentication status immediately
213+
// This ensures Login button appears right away instead of waiting for SSE
214+
if (server) {
215+
server.authenticated = false
216+
}
217+
210218
const response = await api.triggerOAuthLogout(serverName)
211219
if (response.success) {
212220
// The SSE event will trigger a full refresh with actual state
213221
return true
214222
} else {
223+
// Revert optimistic update on error
224+
if (server) {
225+
server.authenticated = true
226+
}
215227
throw new Error(response.error || 'Failed to trigger OAuth logout')
216228
}
217229
} catch (error) {
218230
console.error('Failed to trigger OAuth logout:', error)
231+
// Revert optimistic update on error
232+
const server = servers.value.find(s => s.name === serverName)
233+
if (server) {
234+
server.authenticated = true
235+
}
219236
throw error
220237
}
221238
}

frontend/src/types/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export interface Server {
2424
auth_url: string
2525
token_url: string
2626
}
27+
oauth_status?: 'authenticated' | 'expired' | 'error' | 'none'
28+
token_expires_at?: string
29+
user_logged_out?: boolean // True if user explicitly logged out (prevents auto-reconnection)
2730
}
2831

2932
// Tool Annotation types

0 commit comments

Comments
 (0)