Merge pull request #113 from y574444354/opt/mcp-detector
opt/mcp detector
This commit is contained in:
commit
8f1d3cebd9
|
|
@ -1,72 +1,67 @@
|
||||||
import figures from 'figures'
|
import figures from 'figures';
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react';
|
||||||
import type { CommandResultDisplay } from '../../commands.js'
|
import type { CommandResultDisplay } from '../../commands.js';
|
||||||
import { Box, color, Link, Text, useTheme } from '@anthropic/ink'
|
import { Box, color, Link, Text, useTheme } from '@anthropic/ink';
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||||
import type { ConfigScope } from '../../services/mcp/types.js'
|
import type { ConfigScope } from '../../services/mcp/types.js';
|
||||||
import { describeMcpConfigFilePath } from '../../services/mcp/utils.js'
|
import { describeMcpConfigFilePath } from '../../services/mcp/utils.js';
|
||||||
import { isDebugMode } from '../../utils/debug.js'
|
import { isDebugMode } from '../../utils/debug.js';
|
||||||
import { plural } from '../../utils/stringUtils.js'
|
import { plural } from '../../utils/stringUtils.js';
|
||||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||||
import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink'
|
import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink';
|
||||||
import { McpParsingWarnings } from './McpParsingWarnings.js'
|
import { McpParsingWarnings } from './McpParsingWarnings.js';
|
||||||
import type { AgentMcpServerInfo, ServerInfo } from './types.js'
|
import type { AgentMcpServerInfo, ServerInfo } from './types.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
servers: ServerInfo[]
|
servers: ServerInfo[];
|
||||||
agentServers?: AgentMcpServerInfo[]
|
agentServers?: AgentMcpServerInfo[];
|
||||||
onSelectServer: (server: ServerInfo) => void
|
onSelectServer: (server: ServerInfo) => void;
|
||||||
onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void
|
onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void;
|
||||||
onComplete: (
|
onComplete: (result?: string, options?: { display?: CommandResultDisplay }) => void;
|
||||||
result?: string,
|
defaultTab?: string;
|
||||||
options?: { display?: CommandResultDisplay },
|
};
|
||||||
) => void
|
|
||||||
defaultTab?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectableItem =
|
type SelectableItem =
|
||||||
| { type: 'server'; server: ServerInfo }
|
| { type: 'server'; server: ServerInfo }
|
||||||
| { type: 'agent-server'; agentServer: AgentMcpServerInfo }
|
| { type: 'agent-server'; agentServer: AgentMcpServerInfo };
|
||||||
|
|
||||||
// Define scope order for display (constant, outside component)
|
// Define scope order for display (constant, outside component)
|
||||||
// 'dynamic' (built-in) is rendered separately at the end
|
// 'dynamic' (built-in) is rendered separately at the end
|
||||||
const SCOPE_ORDER: ConfigScope[] = ['project', 'local', 'user', 'enterprise']
|
const SCOPE_ORDER: ConfigScope[] = ['project', 'local', 'user', 'enterprise'];
|
||||||
|
|
||||||
// Get scope heading parts (label is bold, path is grey)
|
// Get scope heading parts (label is bold, path is grey)
|
||||||
function getScopeHeading(scope: ConfigScope): { label: string; path?: string } {
|
function getScopeHeading(scope: ConfigScope): { label: string; path?: string } {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case 'project':
|
case 'project':
|
||||||
return { label: 'Project MCPs', path: describeMcpConfigFilePath(scope) }
|
return { label: 'Project MCPs', path: describeMcpConfigFilePath(scope) };
|
||||||
case 'user':
|
case 'user':
|
||||||
return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) }
|
return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) };
|
||||||
case 'local':
|
case 'local':
|
||||||
return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) }
|
return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) };
|
||||||
case 'enterprise':
|
case 'enterprise':
|
||||||
return { label: 'Enterprise MCPs' }
|
return { label: 'Enterprise MCPs' };
|
||||||
case 'dynamic':
|
case 'dynamic':
|
||||||
return { label: 'Built-in MCPs', path: 'always available' }
|
return { label: 'Built-in MCPs', path: 'always available' };
|
||||||
default:
|
default:
|
||||||
return { label: scope }
|
return { label: scope };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group servers by scope
|
// Group servers by scope
|
||||||
function groupServersByScope(
|
function groupServersByScope(serverList: ServerInfo[]): Map<ConfigScope, ServerInfo[]> {
|
||||||
serverList: ServerInfo[],
|
const groups = new Map<ConfigScope, ServerInfo[]>();
|
||||||
): Map<ConfigScope, ServerInfo[]> {
|
|
||||||
const groups = new Map<ConfigScope, ServerInfo[]>()
|
|
||||||
for (const server of serverList) {
|
for (const server of serverList) {
|
||||||
const scope = server.scope
|
const scope = server.scope;
|
||||||
if (!groups.has(scope)) {
|
if (!groups.has(scope)) {
|
||||||
groups.set(scope, [])
|
groups.set(scope, []);
|
||||||
}
|
}
|
||||||
groups.get(scope)!.push(server)
|
groups.get(scope)!.push(server);
|
||||||
}
|
}
|
||||||
// Sort servers within each group alphabetically
|
// Sort servers within each group alphabetically
|
||||||
for (const [, groupServers] of groups) {
|
for (const [, groupServers] of groups) {
|
||||||
groupServers.sort((a, b) => a.name.localeCompare(b.name))
|
groupServers.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
return groups
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MCPListPanel({
|
export function MCPListPanel({
|
||||||
|
|
@ -76,177 +71,151 @@ export function MCPListPanel({
|
||||||
onSelectAgentServer,
|
onSelectAgentServer,
|
||||||
onComplete,
|
onComplete,
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
const [theme] = useTheme()
|
const [theme] = useTheme();
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
// Non-claudeai servers grouped by scope
|
// Non-claudeai servers grouped by scope
|
||||||
const serversByScope = React.useMemo(() => {
|
const serversByScope = React.useMemo(() => {
|
||||||
const regularServers = servers.filter(
|
const regularServers = servers.filter(s => s.client.config.type !== 'claudeai-proxy');
|
||||||
s => s.client.config.type !== 'claudeai-proxy',
|
return groupServersByScope(regularServers);
|
||||||
)
|
}, [servers]);
|
||||||
return groupServersByScope(regularServers)
|
|
||||||
}, [servers])
|
|
||||||
|
|
||||||
const claudeAiServers = React.useMemo(
|
const claudeAiServers = React.useMemo(
|
||||||
() =>
|
() => servers.filter(s => s.client.config.type === 'claudeai-proxy').sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
servers
|
|
||||||
.filter(s => s.client.config.type === 'claudeai-proxy')
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
||||||
[servers],
|
[servers],
|
||||||
)
|
);
|
||||||
|
|
||||||
// Built-in (dynamic) servers - rendered last
|
// Built-in (dynamic) servers - rendered last
|
||||||
const dynamicServers = React.useMemo(
|
const dynamicServers = React.useMemo(
|
||||||
() =>
|
() => (serversByScope.get('dynamic') ?? []).sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
(serversByScope.get('dynamic') ?? []).sort((a, b) =>
|
|
||||||
a.name.localeCompare(b.name),
|
|
||||||
),
|
|
||||||
[serversByScope],
|
[serversByScope],
|
||||||
)
|
);
|
||||||
|
|
||||||
// Pre-compute dynamic heading for render
|
// Pre-compute dynamic heading for render
|
||||||
const dynamicHeading = getScopeHeading('dynamic')
|
const dynamicHeading = getScopeHeading('dynamic');
|
||||||
|
|
||||||
// Build flat list of selectable items in display order
|
// Build flat list of selectable items in display order
|
||||||
const selectableItems = React.useMemo(() => {
|
const selectableItems = React.useMemo(() => {
|
||||||
const items: SelectableItem[] = []
|
const items: SelectableItem[] = [];
|
||||||
for (const scope of SCOPE_ORDER) {
|
for (const scope of SCOPE_ORDER) {
|
||||||
const scopeServers = serversByScope.get(scope) ?? []
|
const scopeServers = serversByScope.get(scope) ?? [];
|
||||||
for (const server of scopeServers) {
|
for (const server of scopeServers) {
|
||||||
items.push({ type: 'server', server })
|
items.push({ type: 'server', server });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const server of claudeAiServers) {
|
for (const server of claudeAiServers) {
|
||||||
items.push({ type: 'server', server })
|
items.push({ type: 'server', server });
|
||||||
}
|
}
|
||||||
for (const agentServer of agentServers) {
|
for (const agentServer of agentServers) {
|
||||||
items.push({ type: 'agent-server', agentServer })
|
items.push({ type: 'agent-server', agentServer });
|
||||||
}
|
}
|
||||||
// Dynamic (built-in) servers come last
|
// Dynamic (built-in) servers come last
|
||||||
for (const server of dynamicServers) {
|
for (const server of dynamicServers) {
|
||||||
items.push({ type: 'server', server })
|
items.push({ type: 'server', server });
|
||||||
}
|
}
|
||||||
return items
|
return items;
|
||||||
}, [serversByScope, claudeAiServers, agentServers, dynamicServers])
|
}, [serversByScope, claudeAiServers, agentServers, dynamicServers]);
|
||||||
|
|
||||||
const handleCancel = useCallback((): void => {
|
const handleCancel = useCallback((): void => {
|
||||||
onComplete('MCP dialog dismissed', {
|
onComplete('MCP dialog dismissed', {
|
||||||
display: 'system',
|
display: 'system',
|
||||||
})
|
});
|
||||||
}, [onComplete])
|
}, [onComplete]);
|
||||||
|
|
||||||
const handleSelect = useCallback((): void => {
|
const handleSelect = useCallback((): void => {
|
||||||
const item = selectableItems[selectedIndex]
|
const item = selectableItems[selectedIndex];
|
||||||
if (!item) return
|
if (!item) return;
|
||||||
if (item.type === 'server') {
|
if (item.type === 'server') {
|
||||||
onSelectServer(item.server)
|
onSelectServer(item.server);
|
||||||
} else if (item.type === 'agent-server' && onSelectAgentServer) {
|
} else if (item.type === 'agent-server' && onSelectAgentServer) {
|
||||||
onSelectAgentServer(item.agentServer)
|
onSelectAgentServer(item.agentServer);
|
||||||
}
|
}
|
||||||
}, [selectableItems, selectedIndex, onSelectServer, onSelectAgentServer])
|
}, [selectableItems, selectedIndex, onSelectServer, onSelectAgentServer]);
|
||||||
|
|
||||||
// Use configurable keybindings for navigation and selection
|
// Use configurable keybindings for navigation and selection
|
||||||
useKeybindings(
|
useKeybindings(
|
||||||
{
|
{
|
||||||
'confirm:previous': () =>
|
'confirm:previous': () => setSelectedIndex(prev => (prev === 0 ? selectableItems.length - 1 : prev - 1)),
|
||||||
setSelectedIndex(prev =>
|
'confirm:next': () => setSelectedIndex(prev => (prev === selectableItems.length - 1 ? 0 : prev + 1)),
|
||||||
prev === 0 ? selectableItems.length - 1 : prev - 1,
|
|
||||||
),
|
|
||||||
'confirm:next': () =>
|
|
||||||
setSelectedIndex(prev =>
|
|
||||||
prev === selectableItems.length - 1 ? 0 : prev + 1,
|
|
||||||
),
|
|
||||||
'confirm:yes': handleSelect,
|
'confirm:yes': handleSelect,
|
||||||
'confirm:no': handleCancel,
|
'confirm:no': handleCancel,
|
||||||
},
|
},
|
||||||
{ context: 'Confirmation' },
|
{ context: 'Confirmation' },
|
||||||
)
|
);
|
||||||
|
|
||||||
// Build index lookup for each server
|
// Build index lookup for each server
|
||||||
const getServerIndex = (server: ServerInfo): number => {
|
const getServerIndex = (server: ServerInfo): number => {
|
||||||
return selectableItems.findIndex(
|
return selectableItems.findIndex(item => item.type === 'server' && item.server === server);
|
||||||
item => item.type === 'server' && item.server === server,
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAgentServerIndex = (agentServer: AgentMcpServerInfo): number => {
|
const getAgentServerIndex = (agentServer: AgentMcpServerInfo): number => {
|
||||||
return selectableItems.findIndex(
|
return selectableItems.findIndex(item => item.type === 'agent-server' && item.agentServer === agentServer);
|
||||||
item => item.type === 'agent-server' && item.agentServer === agentServer,
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const debugMode = isDebugMode()
|
const debugMode = isDebugMode();
|
||||||
const hasFailedClients = servers.some(s => s.client.type === 'failed')
|
const hasFailedClients = servers.some(s => s.client.type === 'failed');
|
||||||
|
|
||||||
if (servers.length === 0 && agentServers.length === 0) {
|
if (servers.length === 0 && agentServers.length === 0) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderServerItem = (server: ServerInfo): React.ReactNode => {
|
const renderServerItem = (server: ServerInfo): React.ReactNode => {
|
||||||
const index = getServerIndex(server)
|
const index = getServerIndex(server);
|
||||||
const isSelected = selectedIndex === index
|
const isSelected = selectedIndex === index;
|
||||||
let statusIcon = ''
|
let statusIcon = '';
|
||||||
let statusText = ''
|
let statusText = '';
|
||||||
|
|
||||||
if (server.client.type === 'disabled') {
|
if (server.client.type === 'disabled') {
|
||||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
statusIcon = color('inactive', theme)(figures.radioOff);
|
||||||
statusText = 'disabled'
|
statusText = 'disabled';
|
||||||
} else if (server.client.type === 'connected') {
|
} else if (server.client.type === 'connected') {
|
||||||
statusIcon = color('success', theme)(figures.tick)
|
statusIcon = color('success', theme)(figures.tick);
|
||||||
statusText = 'connected'
|
statusText = 'connected';
|
||||||
} else if (server.client.type === 'pending') {
|
} else if (server.client.type === 'pending') {
|
||||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
statusIcon = color('inactive', theme)(figures.radioOff);
|
||||||
const { reconnectAttempt, maxReconnectAttempts } = server.client
|
const { reconnectAttempt, maxReconnectAttempts } = server.client;
|
||||||
if (reconnectAttempt && maxReconnectAttempts) {
|
if (reconnectAttempt && maxReconnectAttempts) {
|
||||||
statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`
|
statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`;
|
||||||
} else {
|
} else {
|
||||||
statusText = 'connecting…'
|
statusText = 'connecting…';
|
||||||
}
|
}
|
||||||
} else if (server.client.type === 'needs-auth') {
|
} else if (server.client.type === 'needs-auth') {
|
||||||
statusIcon = color('warning', theme)(figures.triangleUpOutline)
|
statusIcon = color('warning', theme)(figures.triangleUpOutline);
|
||||||
statusText = 'needs authentication'
|
statusText = 'needs authentication';
|
||||||
} else {
|
} else {
|
||||||
statusIcon = color('error', theme)(figures.cross)
|
statusIcon = color('error', theme)(figures.cross);
|
||||||
statusText = 'failed'
|
statusText = 'failed (may be unavailable)';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`${server.name}-${index}`}>
|
<Box key={`${server.name}-${index}`}>
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
<Text color={isSelected ? 'suggestion' : undefined}>{isSelected ? `${figures.pointer} ` : ' '}</Text>
|
||||||
{isSelected ? `${figures.pointer} ` : ' '}
|
|
||||||
</Text>
|
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>{server.name}</Text>
|
<Text color={isSelected ? 'suggestion' : undefined}>{server.name}</Text>
|
||||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderAgentServerItem = (
|
const renderAgentServerItem = (agentServer: AgentMcpServerInfo): React.ReactNode => {
|
||||||
agentServer: AgentMcpServerInfo,
|
const index = getAgentServerIndex(agentServer);
|
||||||
): React.ReactNode => {
|
const isSelected = selectedIndex === index;
|
||||||
const index = getAgentServerIndex(agentServer)
|
|
||||||
const isSelected = selectedIndex === index
|
|
||||||
const statusIcon = agentServer.needsAuth
|
const statusIcon = agentServer.needsAuth
|
||||||
? color('warning', theme)(figures.triangleUpOutline)
|
? color('warning', theme)(figures.triangleUpOutline)
|
||||||
: color('inactive', theme)(figures.radioOff)
|
: color('inactive', theme)(figures.radioOff);
|
||||||
const statusText = agentServer.needsAuth ? 'may need auth' : 'agent-only'
|
const statusText = agentServer.needsAuth ? 'may need auth' : 'agent-only';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`agent-${agentServer.name}-${index}`}>
|
<Box key={`agent-${agentServer.name}-${index}`}>
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
<Text color={isSelected ? 'suggestion' : undefined}>{isSelected ? `${figures.pointer} ` : ' '}</Text>
|
||||||
{isSelected ? `${figures.pointer} ` : ' '}
|
<Text color={isSelected ? 'suggestion' : undefined}>{agentServer.name}</Text>
|
||||||
</Text>
|
|
||||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
|
||||||
{agentServer.name}
|
|
||||||
</Text>
|
|
||||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const totalServers = servers.length + agentServers.length
|
const totalServers = servers.length + agentServers.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
|
|
@ -261,9 +230,9 @@ export function MCPListPanel({
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{/* Regular servers grouped by scope */}
|
{/* Regular servers grouped by scope */}
|
||||||
{SCOPE_ORDER.map(scope => {
|
{SCOPE_ORDER.map(scope => {
|
||||||
const scopeServers = serversByScope.get(scope)
|
const scopeServers = serversByScope.get(scope);
|
||||||
if (!scopeServers || scopeServers.length === 0) return null
|
if (!scopeServers || scopeServers.length === 0) return null;
|
||||||
const heading = getScopeHeading(scope)
|
const heading = getScopeHeading(scope);
|
||||||
return (
|
return (
|
||||||
<Box key={scope} flexDirection="column" marginBottom={1}>
|
<Box key={scope} flexDirection="column" marginBottom={1}>
|
||||||
<Box paddingLeft={2}>
|
<Box paddingLeft={2}>
|
||||||
|
|
@ -272,7 +241,7 @@ export function MCPListPanel({
|
||||||
</Box>
|
</Box>
|
||||||
{scopeServers.map(server => renderServerItem(server))}
|
{scopeServers.map(server => renderServerItem(server))}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Claude.ai servers section */}
|
{/* Claude.ai servers section */}
|
||||||
|
|
@ -292,18 +261,16 @@ export function MCPListPanel({
|
||||||
<Text bold>Agent MCPs</Text>
|
<Text bold>Agent MCPs</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Group servers by source agent */}
|
{/* Group servers by source agent */}
|
||||||
{[...new Set(agentServers.flatMap(s => s.sourceAgents))].map(
|
{[...new Set(agentServers.flatMap(s => s.sourceAgents))].map(agentName => (
|
||||||
agentName => (
|
<Box key={agentName} flexDirection="column" marginTop={1}>
|
||||||
<Box key={agentName} flexDirection="column" marginTop={1}>
|
<Box paddingLeft={2}>
|
||||||
<Box paddingLeft={2}>
|
<Text dimColor>@{agentName}</Text>
|
||||||
<Text dimColor>@{agentName}</Text>
|
|
||||||
</Box>
|
|
||||||
{agentServers
|
|
||||||
.filter(s => s.sourceAgents.includes(agentName))
|
|
||||||
.map(agentServer => renderAgentServerItem(agentServer))}
|
|
||||||
</Box>
|
</Box>
|
||||||
),
|
{agentServers
|
||||||
)}
|
.filter(s => s.sourceAgents.includes(agentName))
|
||||||
|
.map(agentServer => renderAgentServerItem(agentServer))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -312,9 +279,7 @@ export function MCPListPanel({
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Box paddingLeft={2}>
|
<Box paddingLeft={2}>
|
||||||
<Text bold>{dynamicHeading.label}</Text>
|
<Text bold>{dynamicHeading.label}</Text>
|
||||||
{dynamicHeading.path && (
|
{dynamicHeading.path && <Text dimColor> ({dynamicHeading.path})</Text>}
|
||||||
<Text dimColor> ({dynamicHeading.path})</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
{dynamicServers.map(server => renderServerItem(server))}
|
{dynamicServers.map(server => renderServerItem(server))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -323,17 +288,15 @@ export function MCPListPanel({
|
||||||
{/* Footer info */}
|
{/* Footer info */}
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{hasFailedClients && (
|
{hasFailedClients && (
|
||||||
<Text dimColor>
|
<Box flexDirection="column">
|
||||||
{debugMode
|
<Text dimColor>{'※ Unavailable servers cannot be used until reconnected'}</Text>
|
||||||
? '※ Error logs shown inline with --debug'
|
<Text dimColor>
|
||||||
: '※ Run claude --debug to see error logs'}
|
{debugMode ? '※ Error logs shown inline with --debug' : '※ Run claude --debug to see error logs'}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
<Link url="https://costrict.ai/docs/en/mcp">
|
<Link url="https://costrict.ai/docs/en/mcp">https://costrict.ai/docs/en/mcp</Link> for help
|
||||||
https://costrict.ai/docs/en/mcp
|
|
||||||
</Link>{' '}
|
|
||||||
for help
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -345,15 +308,10 @@ export function MCPListPanel({
|
||||||
<Byline>
|
<Byline>
|
||||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||||
<ConfigurableShortcutHint
|
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />
|
||||||
action="confirm:no"
|
|
||||||
context="Confirmation"
|
|
||||||
fallback="Esc"
|
|
||||||
description="cancel"
|
|
||||||
/>
|
|
||||||
</Byline>
|
</Byline>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user