diff --git a/packages/cli/src/ui/components/mcp/MCPManagementDialog.tsx b/packages/cli/src/ui/components/mcp/MCPManagementDialog.tsx index a79af049b..f00a1fd43 100644 --- a/packages/cli/src/ui/components/mcp/MCPManagementDialog.tsx +++ b/packages/cli/src/ui/components/mcp/MCPManagementDialog.tsx @@ -20,6 +20,7 @@ import { ServerDetailStep } from './steps/ServerDetailStep.js'; import { ToolListStep } from './steps/ToolListStep.js'; import { ToolDetailStep } from './steps/ToolDetailStep.js'; import { DisableScopeSelectStep } from './steps/DisableScopeSelectStep.js'; +import { AuthenticateStep } from './steps/AuthenticateStep.js'; import { useConfig } from '../../contexts/ConfigContext.js'; import { getMCPServerStatus, @@ -225,6 +226,11 @@ export const MCPManagementDialog: React.FC = ({ handleNavigateToStep(MCP_MANAGEMENT_STEPS.TOOL_LIST); }, [handleNavigateToStep]); + // Authenticate + const handleAuthenticate = useCallback(() => { + handleNavigateToStep(MCP_MANAGEMENT_STEPS.AUTHENTICATE); + }, [handleNavigateToStep]); + // Select tool const handleSelectTool = useCallback( (tool: MCPToolDisplayInfo) => { @@ -401,6 +407,9 @@ export const MCPManagementDialog: React.FC = ({ case MCP_MANAGEMENT_STEPS.TOOL_DETAIL: headerText = selectedTool?.name || t('Tool Detail'); break; + case MCP_MANAGEMENT_STEPS.AUTHENTICATE: + headerText = t('OAuth Authentication'); + break; default: headerText = t('MCP Management'); } @@ -435,6 +444,7 @@ export const MCPManagementDialog: React.FC = ({ onViewTools={handleViewTools} onReconnect={handleReconnect} onDisable={handleDisable} + onAuthenticate={handleAuthenticate} onBack={handleNavigateBack} /> ); @@ -463,6 +473,18 @@ export const MCPManagementDialog: React.FC = ({ ); + case MCP_MANAGEMENT_STEPS.AUTHENTICATE: + return ( + { + // TODO: 认证成功后重新加载服务器列表 + handleNavigateBack(); + }} + onBack={handleNavigateBack} + /> + ); + default: return ( @@ -480,6 +502,7 @@ export const MCPManagementDialog: React.FC = ({ handleViewTools, handleReconnect, handleDisable, + handleAuthenticate, handleNavigateBack, handleSelectTool, handleSelectDisableScope, diff --git a/packages/cli/src/ui/components/mcp/steps/AuthenticateStep.tsx b/packages/cli/src/ui/components/mcp/steps/AuthenticateStep.tsx new file mode 100644 index 000000000..a2535ff82 --- /dev/null +++ b/packages/cli/src/ui/components/mcp/steps/AuthenticateStep.tsx @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState } from 'react'; +import { Box, Text } from 'ink'; +import { theme } from '../../../semantic-colors.js'; +import { useKeypress } from '../../../hooks/useKeypress.js'; +import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; +import { t } from '../../../../i18n/index.js'; +import type { AuthenticateStepProps } from '../types.js'; +import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; + +// TODO: 稍后从 utils.ts 导入此函数 +const getOAuthConfigFromServerConfig = (_config: MCPServerConfig): unknown => + null; + +type AuthAction = 'authenticate' | 'back'; + +export const AuthenticateStep: React.FC = ({ + server, + onBack, +}) => { + const [selectedAction, setSelectedAction] = useState('back'); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [authError, setAuthError] = useState(null); + + const actions = [ + { + key: 'authenticate', + label: t('Authenticate'), + value: 'authenticate' as const, + }, + { + key: 'back', + label: t('Back'), + value: 'back' as const, + }, + ]; + + useKeypress( + (key) => { + if (key.name === 'escape') { + onBack(); + } else if (key.name === 'return' && !isAuthenticating) { + switch (selectedAction) { + case 'authenticate': + handleAuthenticate(); + break; + case 'back': + onBack(); + break; + default: + break; + } + } + }, + { isActive: true }, + ); + + const handleAuthenticate = async () => { + if (!server) return; + + setIsAuthenticating(true); + setAuthError(null); + + try { + // TODO: 实现 OAuth 认证逻辑 + // 这里需要调用 MCPOAuthProvider 进行认证 + // 认证成功后调用 onSuccess() + // 认证失败时设置 authError + + // 临时实现:显示提示信息 + setAuthError(t('OAuth authentication is not yet implemented')); + } catch (error) { + setAuthError( + error instanceof Error ? error.message : t('Authentication failed'), + ); + } finally { + setIsAuthenticating(false); + } + }; + + if (!server) { + return ( + + {t('No server selected')} + + ); + } + + const oauthConfig = getOAuthConfigFromServerConfig(server.config); + const hasOAuth = !!oauthConfig; + + return ( + + {/* 认证说明 */} + + + {t('OAuth Authentication')} + + + + + {t('Server:')} {server.name} + + + + {!hasOAuth && ( + + + {t('This server does not have OAuth configuration.')} + + + )} + + {authError && ( + + {authError} + + )} + + + {/* 操作列表 */} + {!hasOAuth ? ( + + + items={actions.filter((a) => a.key === 'back')} + onHighlight={(value: AuthAction) => setSelectedAction(value)} + onSelect={(value: AuthAction) => { + if (value === 'back') { + onBack(); + } + }} + /> + + ) : ( + + + items={actions} + onHighlight={(value: AuthAction) => setSelectedAction(value)} + onSelect={(value: AuthAction) => { + if (value === 'back') { + onBack(); + } + }} + /> + + )} + + {isAuthenticating && ( + + + {t('Authenticating... Please wait.')} + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/mcp/steps/ServerDetailStep.tsx b/packages/cli/src/ui/components/mcp/steps/ServerDetailStep.tsx index 50445d7b7..13d72542c 100644 --- a/packages/cli/src/ui/components/mcp/steps/ServerDetailStep.tsx +++ b/packages/cli/src/ui/components/mcp/steps/ServerDetailStep.tsx @@ -20,13 +20,18 @@ import { // 标签列宽度 const LABEL_WIDTH = 15; -type ServerAction = 'view-tools' | 'reconnect' | 'toggle-disable'; +type ServerAction = + | 'view-tools' + | 'reconnect' + | 'toggle-disable' + | 'authenticate'; export const ServerDetailStep: React.FC = ({ server, onViewTools, onReconnect, onDisable, + onAuthenticate, onBack, }) => { const [selectedAction, setSelectedAction] = @@ -71,6 +76,16 @@ export const ServerDetailStep: React.FC = ({ value: 'toggle-disable', }); + // 如果服务器配置了 OAuth,显示认证选项 + if (server && !server.isDisabled) { + // TODO: 检查服务器是否有 OAuth 配置 + result.push({ + key: 'authenticate', + label: t('Authenticate'), + value: 'authenticate', + }); + } + return result; }, [server]); @@ -89,6 +104,9 @@ export const ServerDetailStep: React.FC = ({ case 'toggle-disable': onDisable?.(); break; + case 'authenticate': + onAuthenticate?.(); + break; default: break; } @@ -228,6 +246,9 @@ export const ServerDetailStep: React.FC = ({ case 'toggle-disable': onDisable?.(); break; + case 'authenticate': + onAuthenticate?.(); + break; default: break; } diff --git a/packages/cli/src/ui/components/mcp/types.ts b/packages/cli/src/ui/components/mcp/types.ts index 1133592bb..34374fa23 100644 --- a/packages/cli/src/ui/components/mcp/types.ts +++ b/packages/cli/src/ui/components/mcp/types.ts @@ -18,6 +18,7 @@ export const MCP_MANAGEMENT_STEPS = { DISABLE_SCOPE_SELECT: 'disable-scope-select', TOOL_LIST: 'tool-list', TOOL_DETAIL: 'tool-detail', + AUTHENTICATE: 'authenticate', // OAuth 认证步骤 } as const; export type MCPManagementStep = @@ -120,7 +121,7 @@ export interface ServerListStepProps { } /** - * ServerDetailStep组件属性 + * ServerDetailStep 组件属性 */ export interface ServerDetailStepProps { /** 选中的服务器 */ @@ -131,6 +132,8 @@ export interface ServerDetailStepProps { onReconnect?: () => void; /** 禁用服务器回调 */ onDisable?: () => void; + /** OAuth 认证回调 */ + onAuthenticate?: () => void; /** 返回回调 */ onBack: () => void; } @@ -162,7 +165,7 @@ export interface ToolListStepProps { } /** - * ToolDetailStep组件属性 + * ToolDetailStep 组件属性 */ export interface ToolDetailStepProps { /** 工具信息 */ @@ -171,6 +174,18 @@ export interface ToolDetailStepProps { onBack: () => void; } +/** + * AuthenticateStep 组件属性 + */ +export interface AuthenticateStepProps { + /** 服务器信息 */ + server: MCPServerDisplayInfo | null; + /** 认证成功回调 */ + onSuccess?: () => void; + /** 返回回调 */ + onBack: () => void; +} + /** * MCP管理对话框属性 */