From c45544c0669ecd7950cc314166419ce651786da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=87=AF90331?= <90331@sangfor.com> Date: Fri, 15 May 2026 16:55:02 +0800 Subject: [PATCH] fix(mcp): add availability hint for failed servers in list panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show 'failed (may be unavailable)' next to disconnected servers instead of plain 'failed' Add footer note explaining unavailable servers cannot be used until reconnected Signed-off-by: 林凯90331 <90331@sangfor.com> Co-authored-by: CoStrict --- src/components/mcp/MCPListPanel.tsx | 286 ++++++++++++---------------- 1 file changed, 122 insertions(+), 164 deletions(-) diff --git a/src/components/mcp/MCPListPanel.tsx b/src/components/mcp/MCPListPanel.tsx index 9b671eefa..abf9ab19e 100644 --- a/src/components/mcp/MCPListPanel.tsx +++ b/src/components/mcp/MCPListPanel.tsx @@ -1,72 +1,67 @@ -import figures from 'figures' -import React, { useCallback, useState } from 'react' -import type { CommandResultDisplay } from '../../commands.js' -import { Box, color, Link, Text, useTheme } from '@anthropic/ink' -import { useKeybindings } from '../../keybindings/useKeybinding.js' -import type { ConfigScope } from '../../services/mcp/types.js' -import { describeMcpConfigFilePath } from '../../services/mcp/utils.js' -import { isDebugMode } from '../../utils/debug.js' -import { plural } from '../../utils/stringUtils.js' -import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' -import { McpParsingWarnings } from './McpParsingWarnings.js' -import type { AgentMcpServerInfo, ServerInfo } from './types.js' +import figures from 'figures'; +import React, { useCallback, useState } from 'react'; +import type { CommandResultDisplay } from '../../commands.js'; +import { Box, color, Link, Text, useTheme } from '@anthropic/ink'; +import { useKeybindings } from '../../keybindings/useKeybinding.js'; +import type { ConfigScope } from '../../services/mcp/types.js'; +import { describeMcpConfigFilePath } from '../../services/mcp/utils.js'; +import { isDebugMode } from '../../utils/debug.js'; +import { plural } from '../../utils/stringUtils.js'; +import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink'; +import { McpParsingWarnings } from './McpParsingWarnings.js'; +import type { AgentMcpServerInfo, ServerInfo } from './types.js'; type Props = { - servers: ServerInfo[] - agentServers?: AgentMcpServerInfo[] - onSelectServer: (server: ServerInfo) => void - onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void - onComplete: ( - result?: string, - options?: { display?: CommandResultDisplay }, - ) => void - defaultTab?: string -} + servers: ServerInfo[]; + agentServers?: AgentMcpServerInfo[]; + onSelectServer: (server: ServerInfo) => void; + onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void; + onComplete: (result?: string, options?: { display?: CommandResultDisplay }) => void; + defaultTab?: string; +}; type SelectableItem = | { type: 'server'; server: ServerInfo } - | { type: 'agent-server'; agentServer: AgentMcpServerInfo } + | { type: 'agent-server'; agentServer: AgentMcpServerInfo }; // Define scope order for display (constant, outside component) // '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) function getScopeHeading(scope: ConfigScope): { label: string; path?: string } { switch (scope) { case 'project': - return { label: 'Project MCPs', path: describeMcpConfigFilePath(scope) } + return { label: 'Project MCPs', path: describeMcpConfigFilePath(scope) }; case 'user': - return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) } + return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) }; case 'local': - return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) } + return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) }; case 'enterprise': - return { label: 'Enterprise MCPs' } + return { label: 'Enterprise MCPs' }; case 'dynamic': - return { label: 'Built-in MCPs', path: 'always available' } + return { label: 'Built-in MCPs', path: 'always available' }; default: - return { label: scope } + return { label: scope }; } } // Group servers by scope -function groupServersByScope( - serverList: ServerInfo[], -): Map { - const groups = new Map() +function groupServersByScope(serverList: ServerInfo[]): Map { + const groups = new Map(); for (const server of serverList) { - const scope = server.scope + const scope = server.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 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({ @@ -76,177 +71,151 @@ export function MCPListPanel({ onSelectAgentServer, onComplete, }: Props): React.ReactNode { - const [theme] = useTheme() - const [selectedIndex, setSelectedIndex] = useState(0) + const [theme] = useTheme(); + const [selectedIndex, setSelectedIndex] = useState(0); // Non-claudeai servers grouped by scope const serversByScope = React.useMemo(() => { - const regularServers = servers.filter( - s => s.client.config.type !== 'claudeai-proxy', - ) - return groupServersByScope(regularServers) - }, [servers]) + const regularServers = servers.filter(s => s.client.config.type !== 'claudeai-proxy'); + return groupServersByScope(regularServers); + }, [servers]); 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], - ) + ); // Built-in (dynamic) servers - rendered last 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], - ) + ); // Pre-compute dynamic heading for render - const dynamicHeading = getScopeHeading('dynamic') + const dynamicHeading = getScopeHeading('dynamic'); // Build flat list of selectable items in display order const selectableItems = React.useMemo(() => { - const items: SelectableItem[] = [] + const items: SelectableItem[] = []; for (const scope of SCOPE_ORDER) { - const scopeServers = serversByScope.get(scope) ?? [] + const scopeServers = serversByScope.get(scope) ?? []; for (const server of scopeServers) { - items.push({ type: 'server', server }) + items.push({ type: 'server', server }); } } for (const server of claudeAiServers) { - items.push({ type: 'server', server }) + items.push({ type: 'server', server }); } for (const agentServer of agentServers) { - items.push({ type: 'agent-server', agentServer }) + items.push({ type: 'agent-server', agentServer }); } // Dynamic (built-in) servers come last for (const server of dynamicServers) { - items.push({ type: 'server', server }) + items.push({ type: 'server', server }); } - return items - }, [serversByScope, claudeAiServers, agentServers, dynamicServers]) + return items; + }, [serversByScope, claudeAiServers, agentServers, dynamicServers]); const handleCancel = useCallback((): void => { onComplete('MCP dialog dismissed', { display: 'system', - }) - }, [onComplete]) + }); + }, [onComplete]); const handleSelect = useCallback((): void => { - const item = selectableItems[selectedIndex] - if (!item) return + const item = selectableItems[selectedIndex]; + if (!item) return; if (item.type === 'server') { - onSelectServer(item.server) + onSelectServer(item.server); } 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 useKeybindings( { - 'confirm:previous': () => - setSelectedIndex(prev => - prev === 0 ? selectableItems.length - 1 : prev - 1, - ), - 'confirm:next': () => - setSelectedIndex(prev => - prev === selectableItems.length - 1 ? 0 : prev + 1, - ), + 'confirm:previous': () => setSelectedIndex(prev => (prev === 0 ? selectableItems.length - 1 : prev - 1)), + 'confirm:next': () => setSelectedIndex(prev => (prev === selectableItems.length - 1 ? 0 : prev + 1)), 'confirm:yes': handleSelect, 'confirm:no': handleCancel, }, { context: 'Confirmation' }, - ) + ); // Build index lookup for each server const getServerIndex = (server: ServerInfo): number => { - return selectableItems.findIndex( - item => item.type === 'server' && item.server === server, - ) - } + return selectableItems.findIndex(item => item.type === 'server' && item.server === server); + }; const getAgentServerIndex = (agentServer: AgentMcpServerInfo): number => { - return selectableItems.findIndex( - item => item.type === 'agent-server' && item.agentServer === agentServer, - ) - } + return selectableItems.findIndex(item => item.type === 'agent-server' && item.agentServer === agentServer); + }; - const debugMode = isDebugMode() - const hasFailedClients = servers.some(s => s.client.type === 'failed') + const debugMode = isDebugMode(); + const hasFailedClients = servers.some(s => s.client.type === 'failed'); if (servers.length === 0 && agentServers.length === 0) { - return null + return null; } const renderServerItem = (server: ServerInfo): React.ReactNode => { - const index = getServerIndex(server) - const isSelected = selectedIndex === index - let statusIcon = '' - let statusText = '' + const index = getServerIndex(server); + const isSelected = selectedIndex === index; + let statusIcon = ''; + let statusText = ''; if (server.client.type === 'disabled') { - statusIcon = color('inactive', theme)(figures.radioOff) - statusText = 'disabled' + statusIcon = color('inactive', theme)(figures.radioOff); + statusText = 'disabled'; } else if (server.client.type === 'connected') { - statusIcon = color('success', theme)(figures.tick) - statusText = 'connected' + statusIcon = color('success', theme)(figures.tick); + statusText = 'connected'; } else if (server.client.type === 'pending') { - statusIcon = color('inactive', theme)(figures.radioOff) - const { reconnectAttempt, maxReconnectAttempts } = server.client + statusIcon = color('inactive', theme)(figures.radioOff); + const { reconnectAttempt, maxReconnectAttempts } = server.client; if (reconnectAttempt && maxReconnectAttempts) { - statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…` + statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`; } else { - statusText = 'connecting…' + statusText = 'connecting…'; } } else if (server.client.type === 'needs-auth') { - statusIcon = color('warning', theme)(figures.triangleUpOutline) - statusText = 'needs authentication' + statusIcon = color('warning', theme)(figures.triangleUpOutline); + statusText = 'needs authentication'; } else { - statusIcon = color('error', theme)(figures.cross) - statusText = 'failed' + statusIcon = color('error', theme)(figures.cross); + statusText = 'failed (may be unavailable)'; } return ( - - {isSelected ? `${figures.pointer} ` : ' '} - + {isSelected ? `${figures.pointer} ` : ' '} {server.name} · {statusIcon} {statusText} - ) - } + ); + }; - const renderAgentServerItem = ( - agentServer: AgentMcpServerInfo, - ): React.ReactNode => { - const index = getAgentServerIndex(agentServer) - const isSelected = selectedIndex === index + const renderAgentServerItem = (agentServer: AgentMcpServerInfo): React.ReactNode => { + const index = getAgentServerIndex(agentServer); + const isSelected = selectedIndex === index; const statusIcon = agentServer.needsAuth ? color('warning', theme)(figures.triangleUpOutline) - : color('inactive', theme)(figures.radioOff) - const statusText = agentServer.needsAuth ? 'may need auth' : 'agent-only' + : color('inactive', theme)(figures.radioOff); + const statusText = agentServer.needsAuth ? 'may need auth' : 'agent-only'; return ( - - {isSelected ? `${figures.pointer} ` : ' '} - - - {agentServer.name} - + {isSelected ? `${figures.pointer} ` : ' '} + {agentServer.name} · {statusIcon} {statusText} - ) - } + ); + }; - const totalServers = servers.length + agentServers.length + const totalServers = servers.length + agentServers.length; return ( @@ -261,9 +230,9 @@ export function MCPListPanel({ {/* Regular servers grouped by scope */} {SCOPE_ORDER.map(scope => { - const scopeServers = serversByScope.get(scope) - if (!scopeServers || scopeServers.length === 0) return null - const heading = getScopeHeading(scope) + const scopeServers = serversByScope.get(scope); + if (!scopeServers || scopeServers.length === 0) return null; + const heading = getScopeHeading(scope); return ( @@ -272,7 +241,7 @@ export function MCPListPanel({ {scopeServers.map(server => renderServerItem(server))} - ) + ); })} {/* Claude.ai servers section */} @@ -292,18 +261,16 @@ export function MCPListPanel({ Agent MCPs {/* Group servers by source agent */} - {[...new Set(agentServers.flatMap(s => s.sourceAgents))].map( - agentName => ( - - - @{agentName} - - {agentServers - .filter(s => s.sourceAgents.includes(agentName)) - .map(agentServer => renderAgentServerItem(agentServer))} + {[...new Set(agentServers.flatMap(s => s.sourceAgents))].map(agentName => ( + + + @{agentName} - ), - )} + {agentServers + .filter(s => s.sourceAgents.includes(agentName)) + .map(agentServer => renderAgentServerItem(agentServer))} + + ))} )} @@ -312,9 +279,7 @@ export function MCPListPanel({ {dynamicHeading.label} - {dynamicHeading.path && ( - ({dynamicHeading.path}) - )} + {dynamicHeading.path && ({dynamicHeading.path})} {dynamicServers.map(server => renderServerItem(server))} @@ -323,17 +288,15 @@ export function MCPListPanel({ {/* Footer info */} {hasFailedClients && ( - - {debugMode - ? '※ Error logs shown inline with --debug' - : '※ Run claude --debug to see error logs'} - + + {'※ Unavailable servers cannot be used until reconnected'} + + {debugMode ? '※ Error logs shown inline with --debug' : '※ Run claude --debug to see error logs'} + + )} - - https://costrict.ai/docs/en/mcp - {' '} - for help + https://costrict.ai/docs/en/mcp for help @@ -345,15 +308,10 @@ export function MCPListPanel({ - + - ) + ); }