mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:46:58 +10:00
177 lines
28 KiB
TypeScript
177 lines
28 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 '../../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 <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_0) {
|
||
|
|
onComplete?.(handleReconnectError(err_0, 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>;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VTdGF0ZSIsIkNvbW1hbmRSZXN1bHREaXNwbGF5IiwidXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzIiwiQm94IiwiY29sb3IiLCJUZXh0IiwidXNlVGhlbWUiLCJnZXRNY3BDb25maWdCeU5hbWUiLCJ1c2VNY3BSZWNvbm5lY3QiLCJ1c2VNY3BUb2dnbGVFbmFibGVkIiwiZGVzY3JpYmVNY3BDb25maWdGaWxlUGF0aCIsImZpbHRlck1jcFByb21wdHNCeVNlcnZlciIsInVzZUFwcFN0YXRlIiwiZXJyb3JNZXNzYWdlIiwiY2FwaXRhbGl6ZSIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIlNlbGVjdCIsIkJ5bGluZSIsIktleWJvYXJkU2hvcnRjdXRIaW50IiwiU3Bpbm5lciIsIkNhcGFiaWxpdGllc1NlY3Rpb24iLCJTdGRpb1NlcnZlckluZm8iLCJoYW5kbGVSZWNvbm5lY3RFcnJvciIsImhhbmRsZVJlY29ubmVjdFJlc3VsdCIsIlByb3BzIiwic2VydmVyIiwic2VydmVyVG9vbHNDb3VudCIsIm9uVmlld1Rvb2xzIiwib25DYW5jZWwiLCJvbkNvbXBsZXRlIiwicmVzdWx0Iiwib3B0aW9ucyIsImRpc3BsYXkiLCJib3JkZXJsZXNzIiwiTUNQU3RkaW9TZXJ2ZXJNZW51IiwiUmVhY3ROb2RlIiwidGhlbWUiLCJleGl0U3RhdGUiLCJtY3AiLCJzIiwicmVjb25uZWN0TWNwU2VydmVyIiwidG9nZ2xlTWNwU2VydmVyIiwiaXNSZWNvbm5lY3RpbmciLCJzZXRJc1JlY29ubmVjdGluZyIsImhhbmRsZVRvZ2dsZUVuYWJsZWQiLCJ1c2VDYWxsYmFjayIsIndhc0VuYWJsZWQiLCJjbGllbnQiLCJ0eXBlIiwibmFtZSIsImVyciIsImFjdGlvbiIsImNhcGl0YWxpemVkU2VydmVyTmFtZSIsIlN0cmluZyIsInNlcnZlckNvbW1hbmRzQ291bnQiLCJjb21tYW5kcyIsImxlbmd0aCIsIm1lbnVPcHRpb25zIiwicHVzaCIsImxhYmVsIiwidmFsdWUiLCJ1bmRlZmluZWQiLCJyYWRpb09mZiIsInRpY2siLCJjcm9zcyIsImNvbmZpZyIsImNvbW1hbmQiLCJhcmdzIiwiam9pbiIsInNjb3BlIiwicmVzb3VyY2VzIiwibWVzc2FnZSIsInBlbmRpbmciLCJrZXlOYW1lIl0sInNvdXJjZXMiOlsiTUNQU3RkaW9TZXJ2ZXJNZW51LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgdHlwZSB7IENvbW1hbmRSZXN1bHREaXNwbGF5IH0gZnJvbSAnLi4vLi4vY29tbWFuZHMuanMnXG5pbXBvcnQgeyB1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MgfSBmcm9tICcuLi8uLi9ob29rcy91c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MuanMnXG5pbXBvcnQgeyBCb3gsIGNvbG9yLCBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGdldE1jcENvbmZpZ0J5TmFtZSB9IGZyb20gJy4uLy4uL3NlcnZpY2VzL21jcC9jb25maWcuanMnXG5pbXBvcnQge1xuICB1c2VNY3BSZWNvbm5lY3QsXG4gIHVzZU1jcFRvZ2dsZUVuYWJsZWQsXG59IGZyb20gJy4uLy4uL3NlcnZpY2VzL21jcC9NQ1BDb25uZWN0aW9uTWFuYWdlci5qcydcbmltcG9ydCB7XG4gIGRlc2NyaWJlTWNwQ29uZmlnRmlsZVBhdGgsXG4gIGZpbHRlck1jcFByb21wdHNCeVNlcnZlcixcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvbWNwL3V0aWxzLmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi8uLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB7IGVycm9yTWVzc2FnZSB9IGZyb20gJy4uLy4uL3V0aWxzL2Vycm9ycy5qcydcbmltcG9ydCB7IGNhcGl0YWxpemUgfSBmcm9tICcuLi8uLi91dGlscy9zdHJpbmdVdGlscy5qcydcbmltcG9ydCB7IENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCB9IGZyb20gJy4uL0NvbmZpZ3VyYWJsZVNob3J0Y3V0SGludC5qcydcbmltcG9ydCB7IFNlbGVjdCB9IGZyb20gJy4uL0N1c3RvbVNlbGVjdC9pbmRleC5qcydcbmltcG9ydCB7IEJ5bGluZSB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vQnlsaW5lLmpzJ1xuaW1wb3J0IHsgS2V5Ym9hcmRTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgU3Bpbm5lciB9IGZyb20gJy4uL1NwaW5uZXIuanMnXG5pbXBvcnQgeyBDYXBhYmlsaXRpZXNTZWN0aW9uIH0gZnJvbSAnLi9DYXBhYmlsaXRpZXNTZWN0aW9uLmpzJ1xuaW1wb3J0IHR5cGUgeyBTdGRpb1NlcnZlckluZm8gfSBmcm9tICcuL3R5cGVzLmpzJ1xuaW1wb3J0IHtcbiAgaGFuZGxlUmVjb25uZWN0RXJyb3IsXG4gIGhhbmRsZVJlY29ubmVjdFJlc3VsdCxcbn0gZnJvbSAnLi91dGlscy9yZWNvbm5lY3RIZWxwZXJzLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBzZXJ2ZXI6IFN0ZGlvU2VydmVySW5mb1xuICBzZXJ2ZXJUb29sc0NvdW50OiBudW1iZXJcbiAgb25WaWV3VG9vbHM6ICgpID0+IHZvaWRcbiAgb25DYW5jZWw6ICgpID0+IHZvaWRcbiAgb25Db21wbGV0ZTogKFxuICAgIHJlc3VsdD86IHN0cmluZyxcbiAgICBvcHRpb25zPzogeyBkaXNwbGF5PzogQ29tbWFuZFJlc3VsdERpc3BsYXkgfSxcbiAgKSA9PiB2b2lkXG4gIGJvcmRlcmxlc3M/OiBib29sZWFuXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBNQ1BTdGRpb1NlcnZlck1lbnUoe1xuICBzZXJ2ZXIsXG4gIHNlcnZlclRvb2xzQ291bnQsXG4gIG9uVmlld1Rvb2xzLFxuICBvbkNhbmNlbCxcbiAgb25Db21wbGV0ZSxcbiAgYm9yZGVybGVzcyA9IGZhbHNlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuICBjb25zdCBleGl0U3RhdGUgPSB1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MoKVxuICBjb25zdCBtY3AgPSB1c2VBcHBTdGF0ZShzID0+IHMubWNwKVxuICBjb25zdCByZWNvbm5lY3RNY3BTZXJ2ZXIgPSB1c2VNY3BSZWNvbm5lY3QoKVxuICBjb25zdCB0b2dnbGVNY3BTZXJ2ZXIgPSB1c2VNY3BUb2dnbGVFbmFibGVkKClcbiAgY29uc3QgW2lzUmVjb25uZWN0aW5nLCBzZXRJc1JlY29
|