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 '../../ink.js'; 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 } from '../design-system/Byline.js'; import { Dialog } from '../design-system/Dialog.js'; import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 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} }