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 } type SelectableItem = | { type: 'server'; server: ServerInfo } | { 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'] // 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) } case 'user': return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) } case 'local': return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) } case 'enterprise': return { label: 'Enterprise MCPs' } case 'dynamic': return { label: 'Built-in MCPs', path: 'always available' } default: return { label: scope } } } // Group servers by scope function groupServersByScope( serverList: ServerInfo[], ): Map { const groups = new Map() for (const server of serverList) { const scope = server.scope if (!groups.has(scope)) { groups.set(scope, []) } 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)) } return groups } export function MCPListPanel({ servers, agentServers = [], onSelectServer, onSelectAgentServer, onComplete, }: Props): React.ReactNode { 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 claudeAiServers = React.useMemo( () => 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], ) // Pre-compute dynamic heading for render const dynamicHeading = getScopeHeading('dynamic') // Build flat list of selectable items in display order const selectableItems = React.useMemo(() => { const items: SelectableItem[] = [] for (const scope of SCOPE_ORDER) { const scopeServers = serversByScope.get(scope) ?? [] for (const server of scopeServers) { items.push({ type: 'server', server }) } } for (const server of claudeAiServers) { items.push({ type: 'server', server }) } for (const agentServer of agentServers) { items.push({ type: 'agent-server', agentServer }) } // Dynamic (built-in) servers come last for (const server of dynamicServers) { items.push({ type: 'server', server }) } return items }, [serversByScope, claudeAiServers, agentServers, dynamicServers]) const handleCancel = useCallback((): void => { onComplete('MCP dialog dismissed', { display: 'system', }) }, [onComplete]) const handleSelect = useCallback((): void => { const item = selectableItems[selectedIndex] if (!item) return if (item.type === 'server') { onSelectServer(item.server) } else if (item.type === 'agent-server' && onSelectAgentServer) { onSelectAgentServer(item.agentServer) } }, [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: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, ) } const getAgentServerIndex = (agentServer: AgentMcpServerInfo): number => { return selectableItems.findIndex( item => item.type === 'agent-server' && item.agentServer === agentServer, ) } const debugMode = isDebugMode() const hasFailedClients = servers.some(s => s.client.type === 'failed') if (servers.length === 0 && agentServers.length === 0) { return null } const renderServerItem = (server: ServerInfo): React.ReactNode => { 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' } else if (server.client.type === '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 if (reconnectAttempt && maxReconnectAttempts) { statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…` } else { statusText = 'connecting…' } } else if (server.client.type === 'needs-auth') { statusIcon = color('warning', theme)(figures.triangleUpOutline) statusText = 'needs authentication' } else { statusIcon = color('error', theme)(figures.cross) statusText = 'failed' } return ( {isSelected ? `${figures.pointer} ` : ' '} {server.name} · {statusIcon} {statusText} ) } 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' return ( {isSelected ? `${figures.pointer} ` : ' '} {agentServer.name} · {statusIcon} {statusText} ) } const totalServers = servers.length + agentServers.length return ( {/* 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) return ( {heading.label} {heading.path && ({heading.path})} {scopeServers.map(server => renderServerItem(server))} ) })} {/* Claude.ai servers section */} {claudeAiServers.length > 0 && ( claude.ai {claudeAiServers.map(server => renderServerItem(server))} )} {/* Agent servers section - grouped by source agent */} {agentServers.length > 0 && ( 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))} ), )} )} {/* Built-in (dynamic) servers section - always last */} {dynamicServers.length > 0 && ( {dynamicHeading.label} {dynamicHeading.path && ( ({dynamicHeading.path}) )} {dynamicServers.map(server => renderServerItem(server))} )} {/* Footer info */} {hasFailedClients && ( {debugMode ? '※ Error logs shown inline with --debug' : '※ Run claude --debug to see error logs'} )} https://code.claude.com/docs/en/mcp {' '} for help {/* Custom footer with navigation hint */} ) }