mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-20 23:35:51 +00:00
244 lines
7.4 KiB
TypeScript
244 lines
7.4 KiB
TypeScript
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 (
|
|
<Box flexDirection="column" gap={1} padding={1}>
|
|
<Text color="text">
|
|
Reconnecting to <Text bold>{server.name}</Text>
|
|
</Text>
|
|
<Box>
|
|
<Spinner />
|
|
<Text> Restarting MCP server process</Text>
|
|
</Box>
|
|
<Text dimColor>This may take a few moments.</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Box
|
|
flexDirection="column"
|
|
paddingX={1}
|
|
borderStyle={borderless ? undefined : 'round'}
|
|
>
|
|
<Box marginBottom={1}>
|
|
<Text bold>{capitalizedServerName} MCP Server</Text>
|
|
</Box>
|
|
|
|
<Box flexDirection="column" gap={0}>
|
|
<Box>
|
|
<Text bold>Status: </Text>
|
|
{server.client.type === 'disabled' ? (
|
|
<Text>{color('inactive', theme)(figures.radioOff)} disabled</Text>
|
|
) : server.client.type === 'connected' ? (
|
|
<Text>{color('success', theme)(figures.tick)} connected</Text>
|
|
) : server.client.type === 'pending' ? (
|
|
<>
|
|
<Text dimColor>{figures.radioOff}</Text>
|
|
<Text> connecting…</Text>
|
|
</>
|
|
) : (
|
|
<Text>{color('error', theme)(figures.cross)} failed</Text>
|
|
)}
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text bold>Command: </Text>
|
|
<Text dimColor>{server.config.command}</Text>
|
|
</Box>
|
|
|
|
{server.config.args && server.config.args.length > 0 && (
|
|
<Box>
|
|
<Text bold>Args: </Text>
|
|
<Text dimColor>{server.config.args.join(' ')}</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<Box>
|
|
<Text bold>Config location: </Text>
|
|
<Text dimColor>
|
|
{describeMcpConfigFilePath(
|
|
getMcpConfigByName(server.name)?.scope ?? 'dynamic',
|
|
)}
|
|
</Text>
|
|
</Box>
|
|
|
|
{server.client.type === 'connected' && (
|
|
<CapabilitiesSection
|
|
serverToolsCount={serverToolsCount}
|
|
serverPromptsCount={serverCommandsCount}
|
|
serverResourcesCount={mcp.resources[server.name]?.length || 0}
|
|
/>
|
|
)}
|
|
|
|
{server.client.type === 'connected' && serverToolsCount > 0 && (
|
|
<Box>
|
|
<Text bold>Tools: </Text>
|
|
<Text dimColor>{serverToolsCount} tools</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{menuOptions.length > 0 && (
|
|
<Box marginTop={1}>
|
|
<Select
|
|
options={menuOptions}
|
|
onChange={async value => {
|
|
if (value === 'tools') {
|
|
onViewTools()
|
|
} else if (value === 'reconnectMcpServer') {
|
|
setIsReconnecting(true)
|
|
try {
|
|
const result = await reconnectMcpServer(server.name)
|
|
const { message } = handleReconnectResult(
|
|
result,
|
|
server.name,
|
|
)
|
|
onComplete?.(message)
|
|
} catch (err) {
|
|
onComplete?.(handleReconnectError(err, server.name))
|
|
} finally {
|
|
setIsReconnecting(false)
|
|
}
|
|
} else if (value === 'toggle-enabled') {
|
|
await handleToggleEnabled()
|
|
} else if (value === 'back') {
|
|
onCancel()
|
|
}
|
|
}}
|
|
onCancel={onCancel}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
<Box marginTop={1}>
|
|
<Text dimColor italic>
|
|
{exitState.pending ? (
|
|
<>Press {exitState.keyName} again to exit</>
|
|
) : (
|
|
<Byline>
|
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
|
<ConfigurableShortcutHint
|
|
action="confirm:no"
|
|
context="Confirmation"
|
|
fallback="Esc"
|
|
description="back"
|
|
/>
|
|
</Byline>
|
|
)}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|