import figures from 'figures' import React, { useCallback, useEffect, useRef, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { Box, color, Link, Text, useTheme } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { AuthenticationCancelledError, performMCPOAuthFlow, } from '../../services/mcp/auth.js' import { capitalize } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../Spinner.js' import type { AgentMcpServerInfo } from './types.js' type Props = { agentServer: AgentMcpServerInfo onCancel: () => void onComplete?: ( result?: string, options?: { display?: CommandResultDisplay }, ) => void } /** * Menu for agent-specific MCP servers. * These servers are defined in agent frontmatter and only connect when the agent runs. * For HTTP/SSE servers, this allows pre-authentication before using the agent. */ export function MCPAgentServerMenu({ agentServer, onCancel, onComplete, }: Props): React.ReactNode { const [theme] = useTheme() const [isAuthenticating, setIsAuthenticating] = useState(false) const [error, setError] = useState(null) const [authorizationUrl, setAuthorizationUrl] = useState(null) const authAbortControllerRef = useRef(null) // Abort OAuth flow on unmount so the callback server is closed even if a // parent component's Esc handler navigates away before ours fires. useEffect(() => () => authAbortControllerRef.current?.abort(), []) // Handle ESC to cancel authentication flow const handleEscCancel = useCallback(() => { if (isAuthenticating) { authAbortControllerRef.current?.abort() authAbortControllerRef.current = null setIsAuthenticating(false) setAuthorizationUrl(null) } }, [isAuthenticating]) useKeybinding('confirm:no', handleEscCancel, { context: 'Confirmation', isActive: isAuthenticating, }) const handleAuthenticate = useCallback(async () => { if (!agentServer.needsAuth || !agentServer.url) { return } setIsAuthenticating(true) setError(null) const controller = new AbortController() authAbortControllerRef.current = controller try { // Create a temporary config for OAuth const tempConfig = { type: agentServer.transport as 'http' | 'sse', url: agentServer.url, } await performMCPOAuthFlow( agentServer.name, tempConfig, setAuthorizationUrl, controller.signal, ) onComplete?.( `Authentication successful for ${agentServer.name}. The server will connect when the agent runs.`, ) } catch (err) { // Don't show error if it was a cancellation if ( err instanceof Error && !(err instanceof AuthenticationCancelledError) ) { setError(err.message) } } finally { setIsAuthenticating(false) authAbortControllerRef.current = null } }, [agentServer, onComplete]) const capitalizedServerName = capitalize(String(agentServer.name)) if (isAuthenticating) { return ( Authenticating with {agentServer.name}… A browser window will open for authentication {authorizationUrl && ( If your browser doesn't open automatically, copy this URL manually: )} Return here after authenticating in your browser.{' '} ) } const menuOptions = [] // Only show authenticate option for HTTP/SSE servers if (agentServer.needsAuth) { menuOptions.push({ label: agentServer.isAuthenticated ? 'Re-authenticate' : 'Authenticate', value: 'auth', }) } menuOptions.push({ label: 'Back', value: 'back', }) return ( exitState.pending ? ( Press {exitState.keyName} again to exit ) : ( ) } > Type: {agentServer.transport} {agentServer.url && ( URL: {agentServer.url} )} {agentServer.command && ( Command: {agentServer.command} )} Used by: {agentServer.sourceAgents.join(', ')} Status: {color('inactive', theme)(figures.radioOff)} not connected (agent-only) {agentServer.needsAuth && ( Auth: {agentServer.isAuthenticated ? ( {color('success', theme)(figures.tick)} authenticated ) : ( {color('warning', theme)(figures.triangleUpOutline)} may need authentication )} )} This server connects only when running the agent. {error && ( Error: {error} )}