Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion examples/chat-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --port=5002",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
Expand Down
48 changes: 29 additions & 19 deletions examples/chat-ui/src/components/McpServerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,15 @@ interface McpServer {
}

// MCP Connection wrapper for a single server
function McpConnection({
server,
onConnectionUpdate,
}: {
server: McpServer
onConnectionUpdate: (serverId: string, data: any) => void
}) {
function McpConnection({ server, onConnectionUpdate }: { server: McpServer; onConnectionUpdate: (serverId: string, data: any) => void }) {
// Use the MCP hook with the server URL
const connection = useMcp({
url: server.url,
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
transportType: server.transportType,
preventAutoAuth: true, // Prevent automatic popups on page load
})

// Update parent component with connection data
Expand Down Expand Up @@ -206,6 +201,8 @@ const McpServerModal: React.FC<McpServerModalProps> = ({ isOpen, onClose, onTool
switch (state) {
case 'discovering':
return <span className={`${baseClasses} bg-blue-100 text-blue-800`}>Discovering</span>
case 'pending_auth':
return <span className={`${baseClasses} bg-orange-100 text-orange-800`}>Authentication Required</span>
case 'authenticating':
return <span className={`${baseClasses} bg-purple-100 text-purple-800`}>Authenticating</span>
case 'connecting':
Expand Down Expand Up @@ -299,18 +296,31 @@ const McpServerModal: React.FC<McpServerModalProps> = ({ isOpen, onClose, onTool
<div className="text-sm text-red-600 p-3 bg-red-50 rounded border mb-3">{error}</div>
)}

{authUrl && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded mb-3">
<p className="text-sm mb-2">Authentication required. Please click the link below:</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-orange-700 hover:text-orange-800 underline"
onClick={() => handleManualAuth(server.id)}
>
Authenticate in new window
</a>
{(state === 'pending_auth' || authUrl) && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded mb-3">
<p className="text-sm mb-2">
{state === 'pending_auth'
? 'Authentication is required to connect to this server.'
: 'Authentication popup was blocked. You can open the authentication page manually:'}
</p>
<div className="space-y-2">
<button
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium"
onClick={() => handleManualAuth(server.id)}
>
Open Authentication Popup
</button>
{authUrl && (
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-center text-sm text-blue-700 hover:text-blue-800 underline"
>
Or open in new tab instead
</a>
)}
</div>
</div>
)}

Expand Down
42 changes: 29 additions & 13 deletions examples/chat-ui/src/components/McpServers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function McpConnection({ serverUrl, onConnectionUpdate }: { serverUrl: string; o
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
preventAutoAuth: true, // Prevent automatic popups on page load
})

// Update parent component with connection data
Expand Down Expand Up @@ -103,6 +104,8 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
switch (state) {
case 'discovering':
return <span className={`${baseClasses} bg-blue-100 text-blue-800`}>Discovering</span>
case 'pending_auth':
return <span className={`${baseClasses} bg-orange-100 text-orange-800`}>Authentication Required</span>
case 'authenticating':
return <span className={`${baseClasses} bg-purple-100 text-purple-800`}>Authenticating</span>
case 'connecting':
Expand Down Expand Up @@ -184,19 +187,32 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
)}
</div>

{/* Authentication Link if needed */}
{authUrl && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded">
<p className="text-xs mb-2">Authentication required. Please click the link below:</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-orange-700 hover:text-orange-800 underline"
onClick={handleManualAuth}
>
Authenticate in new window
</a>
{/* Authentication Action for pending_auth or existing authUrl */}
{(state === 'pending_auth' || authUrl) && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded">
<p className="text-xs mb-2">
{state === 'pending_auth'
? 'Authentication is required to connect to this server.'
: 'Authentication popup was blocked. You can open the authentication page manually:'}
</p>
<div className="space-y-2">
<button
className="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
onClick={handleManualAuth}
>
Open Authentication Popup
</button>
{authUrl && (
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-center text-xs text-blue-700 hover:text-blue-800 underline"
>
Or open in new tab instead
</a>
)}
</div>
</div>
)}

Expand Down
2 changes: 1 addition & 1 deletion examples/inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --port=5001",
"build": "tsc -b && pnpm run build:only",
"build:only": "vite build",
"lint": "eslint .",
Expand Down
19 changes: 16 additions & 3 deletions src/auth/browser-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
}

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

return sanitizedAuthUrl
}

/**
* Redirects the user agent to the authorization URL, storing necessary state.
* This now adheres to the SDK's void return type expectation for the interface.
* @param authorizationUrl The fully constructed authorization URL from the SDK.
*/
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
// Prepare the authorization URL with state
const sanitizedAuthUrl = await this.prepareAuthorizationUrl(authorizationUrl)

// Attempt to open the popup
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed
try {
Expand Down
5 changes: 4 additions & 1 deletion src/react/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type UseMcpOptions = {
popupFeatures?: string
/** Transport type preference: 'auto' (HTTP with SSE fallback), 'http' (HTTP only), 'sse' (SSE only) */
transportType?: 'auto' | 'http' | 'sse'
/** Prevent automatic authentication popup on initial connection (default: false) */
preventAutoAuth?: boolean
}

export type UseMcpResult = {
Expand All @@ -36,13 +38,14 @@ export type UseMcpResult = {
/**
* The current state of the MCP connection:
* - 'discovering': Checking server existence and capabilities (including auth requirements).
* - 'pending_auth': Authentication is required but auto-popup was prevented. User action needed.
* - 'authenticating': Authentication is required and the process (e.g., popup) has been initiated.
* - 'connecting': Establishing the SSE connection to the server.
* - 'loading': Connected; loading resources like the tool list.
* - 'ready': Connected and ready for tool calls.
* - 'failed': Connection or authentication failed. Check the `error` property.
*/
state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
/** If the state is 'failed', this provides the error message */
error?: string
/**
Expand Down
54 changes: 49 additions & 5 deletions src/react/useMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
autoRetry = false,
autoReconnect = DEFAULT_RECONNECT_DELAY,
transportType = 'auto',
preventAutoAuth = false,
} = options

const [state, setState] = useState<UseMcpResult['state']>('discovering')
Expand Down Expand Up @@ -326,9 +327,22 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {

// Check for Auth error (Simplified - requires more thought for interaction with fallback)
if (errorInstance instanceof UnauthorizedError || errorMessage.includes('Unauthorized') || errorMessage.includes('401')) {
addLog('info', 'Authentication required. Initiating SDK auth flow...')
addLog('info', 'Authentication required.')

// Check if we have existing tokens before triggering auth flow
assert(authProviderRef.current, 'Auth Provider not available for auth flow')
const existingTokens = await authProviderRef.current.tokens()

// If preventAutoAuth is enabled and no valid tokens exist, go to pending_auth state
if (preventAutoAuth && !existingTokens) {
addLog('info', 'Authentication required but auto-auth prevented. User action needed.')
setState('pending_auth')
// We'll set the auth URL when the user manually triggers auth
return 'auth_redirect' // Signal that we need user action
}

// Ensure state is set only once if multiple attempts trigger auth
if (stateRef.current !== 'authenticating') {
if (stateRef.current !== 'authenticating' && stateRef.current !== 'pending_auth') {
setState('authenticating')
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
authTimeoutRef.current = setTimeout(() => {
Expand All @@ -337,7 +351,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
}

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

if (!isMountedRef.current) return 'failed' // Unmounted during auth
Expand Down Expand Up @@ -519,13 +532,44 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
}, [addLog, connect]) // Depends only on stable callbacks

// authenticate is stable (depends on stable addLog, retry, connect)
const authenticate = useCallback(() => {
const authenticate = useCallback(async () => {
addLog('info', 'Manual authentication requested...')
const currentState = stateRef.current // Use ref

if (currentState === 'failed') {
addLog('info', 'Attempting to reconnect and authenticate via retry...')
retry()
} else if (currentState === 'pending_auth') {
addLog('info', 'Proceeding with authentication from pending state...')
setState('authenticating')
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
authTimeoutRef.current = setTimeout(() => {
/* ... timeout logic ... */
}, AUTH_TIMEOUT)

try {
assert(authProviderRef.current, 'Auth Provider not available for manual auth')
const authResult = await auth(authProviderRef.current, { serverUrl: url })

if (!isMountedRef.current) return

if (authResult === 'AUTHORIZED') {
addLog('info', 'Manual authentication successful. Re-attempting connection...')
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
connectingRef.current = false
connect() // Restart full connection sequence
} else if (authResult === 'REDIRECT') {
addLog('info', 'Redirecting for manual authentication. Waiting for callback...')
// State is already authenticating, wait for callback
}
} catch (authError) {
if (!isMountedRef.current) return
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
failConnection(
`Manual authentication failed: ${authError instanceof Error ? authError.message : String(authError)}`,
authError instanceof Error ? authError : undefined,
)
}
} else if (currentState === 'authenticating') {
addLog('warn', 'Already attempting authentication. Check for blocked popups or wait for timeout.')
const manualUrl = authProviderRef.current?.getLastAttemptedAuthUrl()
Expand All @@ -545,7 +589,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
// assert(authProviderRef.current, "Auth Provider not available");
// auth(authProviderRef.current, { serverUrl: url }).catch(failConnection);
}
}, [addLog, retry, authUrl]) // Depends on stable callbacks and authUrl state
}, [addLog, retry, authUrl, url, failConnection, connect]) // Depends on stable callbacks and authUrl state

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