Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit b3a62f2

Browse files
committed
feat: add preventAutoAuth option to prevent automatic authentication popups
- Add preventAutoAuth option to UseMcpOptions to control automatic popup behavior - Introduce pending_auth state for when auth is required but auto-popup is prevented - Enhance authentication flow to check for existing tokens before triggering auth - Add prepareAuthorizationUrl() helper method in BrowserOAuthClientProvider - Update authenticate() method to handle pending_auth state transitions - Improve chat UI components to handle new authentication states gracefully - Add clear call-to-action UI with primary popup button and fallback new tab link - Maintain backward compatibility - existing code continues to work unchanged This prevents browser popup blockers from interfering with authentication flows when users return to pages with MCP servers, providing better UX and control.
1 parent 43dc39d commit b3a62f2

File tree

5 files changed

+132
-34
lines changed

5 files changed

+132
-34
lines changed

examples/chat-ui/src/components/McpServerModal.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function McpConnection({
2323
debug: true,
2424
autoRetry: false,
2525
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
26+
preventAutoAuth: true, // Prevent automatic popups on page load
2627
})
2728

2829
// Update parent component with connection data
@@ -197,6 +198,12 @@ const McpServerModal: React.FC<McpServerModalProps> = ({
197198
Discovering
198199
</span>
199200
)
201+
case 'pending_auth':
202+
return (
203+
<span className={`${baseClasses} bg-orange-100 text-orange-800`}>
204+
Authentication Required
205+
</span>
206+
)
200207
case 'authenticating':
201208
return (
202209
<span className={`${baseClasses} bg-purple-100 text-purple-800`}>
@@ -320,20 +327,32 @@ const McpServerModal: React.FC<McpServerModalProps> = ({
320327
</div>
321328
)}
322329

323-
{authUrl && (
324-
<div className="p-3 bg-orange-50 border border-orange-200 rounded mb-3">
330+
{(state === 'pending_auth' || authUrl) && (
331+
<div className="p-3 bg-blue-50 border border-blue-200 rounded mb-3">
325332
<p className="text-sm mb-2">
326-
Authentication required. Please click the link below:
333+
{state === 'pending_auth'
334+
? 'Authentication is required to connect to this server.'
335+
: 'Authentication popup was blocked. You can open the authentication page manually:'
336+
}
327337
</p>
328-
<a
329-
href={authUrl}
330-
target="_blank"
331-
rel="noopener noreferrer"
332-
className="text-sm text-orange-700 hover:text-orange-800 underline"
333-
onClick={() => handleManualAuth(server.id)}
334-
>
335-
Authenticate in new window
336-
</a>
338+
<div className="space-y-2">
339+
<button
340+
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium"
341+
onClick={() => handleManualAuth(server.id)}
342+
>
343+
Open Authentication Popup
344+
</button>
345+
{authUrl && (
346+
<a
347+
href={authUrl}
348+
target="_blank"
349+
rel="noopener noreferrer"
350+
className="block text-center text-sm text-blue-700 hover:text-blue-800 underline"
351+
>
352+
Or open in new tab instead
353+
</a>
354+
)}
355+
</div>
337356
</div>
338357
)}
339358

examples/chat-ui/src/components/McpServers.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function McpConnection({
1616
debug: true,
1717
autoRetry: false,
1818
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
19+
preventAutoAuth: true, // Prevent automatic popups on page load
1920
})
2021

2122
// Update parent component with connection data
@@ -127,6 +128,12 @@ export function McpServers({
127128
Discovering
128129
</span>
129130
)
131+
case 'pending_auth':
132+
return (
133+
<span className={`${baseClasses} bg-orange-100 text-orange-800`}>
134+
Authentication Required
135+
</span>
136+
)
130137
case 'authenticating':
131138
return (
132139
<span className={`${baseClasses} bg-purple-100 text-purple-800`}>
@@ -241,21 +248,33 @@ export function McpServers({
241248
)}
242249
</div>
243250

244-
{/* Authentication Link if needed */}
245-
{authUrl && (
246-
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
251+
{/* Authentication Action for pending_auth or existing authUrl */}
252+
{(state === 'pending_auth' || authUrl) && (
253+
<div className="p-3 bg-blue-50 border border-blue-200 rounded">
247254
<p className="text-xs mb-2">
248-
Authentication required. Please click the link below:
255+
{state === 'pending_auth'
256+
? 'Authentication is required to connect to this server.'
257+
: 'Authentication popup was blocked. You can open the authentication page manually:'
258+
}
249259
</p>
250-
<a
251-
href={authUrl}
252-
target="_blank"
253-
rel="noopener noreferrer"
254-
className="text-xs text-orange-700 hover:text-orange-800 underline"
255-
onClick={handleManualAuth}
256-
>
257-
Authenticate in new window
258-
</a>
260+
<div className="space-y-2">
261+
<button
262+
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
263+
onClick={handleManualAuth}
264+
>
265+
Open Authentication Popup
266+
</button>
267+
{authUrl && (
268+
<a
269+
href={authUrl}
270+
target="_blank"
271+
rel="noopener noreferrer"
272+
className="block text-center text-xs text-blue-700 hover:text-blue-800 underline"
273+
>
274+
Or open in new tab instead
275+
</a>
276+
)}
277+
</div>
259278
</div>
260279
)}
261280

src/auth/browser-provider.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
115115
}
116116

117117
/**
118-
* Redirects the user agent to the authorization URL, storing necessary state.
119-
* This now adheres to the SDK's void return type expectation for the interface.
118+
* Generates and stores the authorization URL with state, without opening a popup.
119+
* Used when preventAutoAuth is enabled to provide the URL for manual navigation.
120120
* @param authorizationUrl The fully constructed authorization URL from the SDK.
121+
* @returns The full authorization URL with state parameter.
121122
*/
122-
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
123+
async prepareAuthorizationUrl(authorizationUrl: URL): Promise<string> {
123124
// Generate a unique state parameter for this authorization request
124125
const state = crypto.randomUUID()
125126
const stateKey = `${this.storageKeyPrefix}:state_${state}`
@@ -146,6 +147,18 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
146147
// Persist the exact auth URL in case the popup fails and manual navigation is needed
147148
localStorage.setItem(this.getKey('last_auth_url'), authUrlString)
148149

150+
return authUrlString
151+
}
152+
153+
/**
154+
* Redirects the user agent to the authorization URL, storing necessary state.
155+
* This now adheres to the SDK's void return type expectation for the interface.
156+
* @param authorizationUrl The fully constructed authorization URL from the SDK.
157+
*/
158+
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
159+
// Prepare the authorization URL with state
160+
const authUrlString = await this.prepareAuthorizationUrl(authorizationUrl)
161+
149162
// Attempt to open the popup
150163
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed
151164
try {

src/react/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export type UseMcpOptions = {
2626
autoReconnect?: boolean | number
2727
/** Popup window features string (dimensions and behavior) for OAuth */
2828
popupFeatures?: string
29+
/** Prevent automatic authentication popup on initial connection (default: false) */
30+
preventAutoAuth?: boolean
2931
}
3032

3133
export type UseMcpResult = {
@@ -34,13 +36,14 @@ export type UseMcpResult = {
3436
/**
3537
* The current state of the MCP connection:
3638
* - 'discovering': Checking server existence and capabilities (including auth requirements).
39+
* - 'pending_auth': Authentication is required but auto-popup was prevented. User action needed.
3740
* - 'authenticating': Authentication is required and the process (e.g., popup) has been initiated.
3841
* - 'connecting': Establishing the SSE connection to the server.
3942
* - 'loading': Connected; loading resources like the tool list.
4043
* - 'ready': Connected and ready for tool calls.
4144
* - 'failed': Connection or authentication failed. Check the `error` property.
4245
*/
43-
state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
46+
state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
4447
/** If the state is 'failed', this provides the error message */
4548
error?: string
4649
/**

src/react/useMcp.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
3030
debug = false,
3131
autoRetry = false,
3232
autoReconnect = DEFAULT_RECONNECT_DELAY,
33+
preventAutoAuth = false,
3334
} = options
3435

3536
const [state, setState] = useState<UseMcpResult['state']>('discovering')
@@ -288,9 +289,22 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
288289

289290
// Check for Auth error (Simplified - requires more thought for interaction with fallback)
290291
if (errorInstance instanceof UnauthorizedError || errorMessage.includes('Unauthorized') || errorMessage.includes('401')) {
291-
addLog('info', 'Authentication required. Initiating SDK auth flow...')
292+
addLog('info', 'Authentication required.')
293+
294+
// Check if we have existing tokens before triggering auth flow
295+
assert(authProviderRef.current, 'Auth Provider not available for auth flow')
296+
const existingTokens = await authProviderRef.current.tokens()
297+
298+
// If preventAutoAuth is enabled and no valid tokens exist, go to pending_auth state
299+
if (preventAutoAuth && !existingTokens) {
300+
addLog('info', 'Authentication required but auto-auth prevented. User action needed.')
301+
setState('pending_auth')
302+
// We'll set the auth URL when the user manually triggers auth
303+
return 'auth_redirect' // Signal that we need user action
304+
}
305+
292306
// Ensure state is set only once if multiple attempts trigger auth
293-
if (stateRef.current !== 'authenticating') {
307+
if (stateRef.current !== 'authenticating' && stateRef.current !== 'pending_auth') {
294308
setState('authenticating')
295309
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
296310
authTimeoutRef.current = setTimeout(() => {
@@ -299,7 +313,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
299313
}
300314

301315
try {
302-
assert(authProviderRef.current, 'Auth Provider not available for auth flow')
303316
const authResult = await auth(authProviderRef.current, { serverUrl: url })
304317

305318
if (!isMountedRef.current) return 'failed' // Unmounted during auth
@@ -478,13 +491,44 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
478491
}, [addLog, connect]) // Depends only on stable callbacks
479492

480493
// authenticate is stable (depends on stable addLog, retry, connect)
481-
const authenticate = useCallback(() => {
494+
const authenticate = useCallback(async () => {
482495
addLog('info', 'Manual authentication requested...')
483496
const currentState = stateRef.current // Use ref
484497

485498
if (currentState === 'failed') {
486499
addLog('info', 'Attempting to reconnect and authenticate via retry...')
487500
retry()
501+
} else if (currentState === 'pending_auth') {
502+
addLog('info', 'Proceeding with authentication from pending state...')
503+
setState('authenticating')
504+
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
505+
authTimeoutRef.current = setTimeout(() => {
506+
/* ... timeout logic ... */
507+
}, AUTH_TIMEOUT)
508+
509+
try {
510+
assert(authProviderRef.current, 'Auth Provider not available for manual auth')
511+
const authResult = await auth(authProviderRef.current, { serverUrl: url })
512+
513+
if (!isMountedRef.current) return
514+
515+
if (authResult === 'AUTHORIZED') {
516+
addLog('info', 'Manual authentication successful. Re-attempting connection...')
517+
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
518+
connectingRef.current = false
519+
connect() // Restart full connection sequence
520+
} else if (authResult === 'REDIRECT') {
521+
addLog('info', 'Redirecting for manual authentication. Waiting for callback...')
522+
// State is already authenticating, wait for callback
523+
}
524+
} catch (authError) {
525+
if (!isMountedRef.current) return
526+
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
527+
failConnection(
528+
`Manual authentication failed: ${authError instanceof Error ? authError.message : String(authError)}`,
529+
authError instanceof Error ? authError : undefined,
530+
)
531+
}
488532
} else if (currentState === 'authenticating') {
489533
addLog('warn', 'Already attempting authentication. Check for blocked popups or wait for timeout.')
490534
const manualUrl = authProviderRef.current?.getLastAttemptedAuthUrl()
@@ -504,7 +548,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
504548
// assert(authProviderRef.current, "Auth Provider not available");
505549
// auth(authProviderRef.current, { serverUrl: url }).catch(failConnection);
506550
}
507-
}, [addLog, retry, authUrl]) // Depends on stable callbacks and authUrl state
551+
}, [addLog, retry, authUrl, url, failConnection, connect]) // Depends on stable callbacks and authUrl state
508552

509553
// clearStorage is stable (depends on stable addLog, disconnect)
510554
const clearStorage = useCallback(() => {

0 commit comments

Comments
 (0)