import figures from 'figures' import React, { useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' import { Box, color, Text, useTheme } from '@anthropic/ink' import { getMcpConfigByName } from '../../services/mcp/config.js' import { useMcpReconnect, useMcpToggleEnabled, } from '../../services/mcp/MCPConnectionManager.js' import { describeMcpConfigFilePath, filterMcpPromptsByServer, } from '../../services/mcp/utils.js' import { useAppState } from '../../state/AppState.js' import { errorMessage } from '../../utils/errors.js' import { capitalize } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../Spinner.js' import { CapabilitiesSection } from './CapabilitiesSection.js' import type { StdioServerInfo } from './types.js' import { handleReconnectError, handleReconnectResult, } from './utils/reconnectHelpers.js' type Props = { server: StdioServerInfo serverToolsCount: number onViewTools: () => void onCancel: () => void onComplete: ( result?: string, options?: { display?: CommandResultDisplay }, ) => void borderless?: boolean } export function MCPStdioServerMenu({ server, serverToolsCount, onViewTools, onCancel, onComplete, borderless = false, }: Props): React.ReactNode { const [theme] = useTheme() const exitState = useExitOnCtrlCDWithKeybindings() const mcp = useAppState(s => s.mcp) const reconnectMcpServer = useMcpReconnect() const toggleMcpServer = useMcpToggleEnabled() const [isReconnecting, setIsReconnecting] = useState(false) const handleToggleEnabled = React.useCallback(async () => { const wasEnabled = server.client.type !== 'disabled' try { await toggleMcpServer(server.name) // Return to the server list so user can continue managing other servers onCancel() } catch (err) { const action = wasEnabled ? 'disable' : 'enable' onComplete( `Failed to ${action} MCP server '${server.name}': ${errorMessage(err)}`, ) } }, [server.client.type, server.name, toggleMcpServer, onCancel, onComplete]) const capitalizedServerName = capitalize(String(server.name)) // Count MCP prompts for this server (skills are shown in /skills, not here) const serverCommandsCount = filterMcpPromptsByServer( mcp.commands, server.name, ).length const menuOptions = [] // Only show "View tools" if server is not disabled and has tools if (server.client.type !== 'disabled' && serverToolsCount > 0) { menuOptions.push({ label: 'View tools', value: 'tools', }) } // Only show reconnect option if the server is not disabled if (server.client.type !== 'disabled') { menuOptions.push({ label: 'Reconnect', value: 'reconnectMcpServer', }) } menuOptions.push({ label: server.client.type !== 'disabled' ? 'Disable' : 'Enable', value: 'toggle-enabled', }) // If there are no other options, add a back option so Select handles escape if (menuOptions.length === 0) { menuOptions.push({ label: 'Back', value: 'back', }) } if (isReconnecting) { return ( Reconnecting to {server.name} Restarting MCP server process This may take a few moments. ) } return ( {capitalizedServerName} MCP Server Status: {server.client.type === 'disabled' ? ( {color('inactive', theme)(figures.radioOff)} disabled ) : server.client.type === 'connected' ? ( {color('success', theme)(figures.tick)} connected ) : server.client.type === 'pending' ? ( <> {figures.radioOff} connecting… ) : ( {color('error', theme)(figures.cross)} failed )} Command: {server.config.command} {server.config.args && server.config.args.length > 0 && ( Args: {server.config.args.join(' ')} )} Config location: {describeMcpConfigFilePath( getMcpConfigByName(server.name)?.scope ?? 'dynamic', )} {server.client.type === 'connected' && ( )} {server.client.type === 'connected' && serverToolsCount > 0 && ( Tools: {serverToolsCount} tools )} {menuOptions.length > 0 && (