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