mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
Merge pull request #2327 from QwenLM/fix-mcp-auth
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
feat(mcp): improve OAuth auth UX - post-auth feedback, i18n, clear auth, and bug fixes
This commit is contained in:
commit
46d57afb22
13 changed files with 252 additions and 24 deletions
|
|
@ -745,6 +745,15 @@ export default {
|
|||
"Authentifizierung mit MCP-Server '{{name}}' fehlgeschlagen: {{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"Werkzeuge von '{{name}}' werden neu erkannt...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"{{count}} Werkzeug(e) von '{{name}}' entdeckt.",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'Authentifizierung abgeschlossen. Zurück zu den Serverdetails...',
|
||||
'Authentication successful.': 'Authentifizierung erfolgreich.',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'Falls der Browser sich nicht öffnet, kopieren Sie diese URL und fügen Sie sie in Ihren Browser ein:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'⚠️ Stellen Sie sicher, dass Sie die VOLLSTÄNDIGE URL kopieren – sie kann über mehrere Zeilen gehen.',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - Chat
|
||||
|
|
@ -916,6 +925,8 @@ export default {
|
|||
Disable: 'Deaktivieren',
|
||||
Enable: 'Aktivieren',
|
||||
Authenticate: 'Authentifizieren',
|
||||
'Re-authenticate': 'Erneut authentifizieren',
|
||||
'Clear Authentication': 'Authentifizierung löschen',
|
||||
disabled: 'deaktiviert',
|
||||
'Server:': 'Server:',
|
||||
Reconnect: 'Neu verbinden',
|
||||
|
|
|
|||
|
|
@ -811,6 +811,15 @@ export default {
|
|||
"Failed to authenticate with MCP server '{{name}}': {{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"Re-discovering tools from '{{name}}'...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'Authentication complete. Returning to server details...',
|
||||
'Authentication successful.': 'Authentication successful.',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'If the browser does not open, copy and paste this URL into your browser:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.',
|
||||
|
||||
// ============================================================================
|
||||
// MCP Management Dialog
|
||||
|
|
@ -843,6 +852,8 @@ export default {
|
|||
Enable: 'Enable',
|
||||
Disable: 'Disable',
|
||||
Authenticate: 'Authenticate',
|
||||
'Re-authenticate': 'Re-authenticate',
|
||||
'Clear Authentication': 'Clear Authentication',
|
||||
'Server:': 'Server:',
|
||||
'Command:': 'Command:',
|
||||
'Working Directory:': 'Working Directory:',
|
||||
|
|
|
|||
|
|
@ -507,6 +507,15 @@ export default {
|
|||
"MCPサーバー '{{name}}' での認証に失敗: {{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"'{{name}}' からツールを再検出中...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"'{{name}}' から {{count}} 個のツールを検出しました。",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'認証完了。サーバー詳細に戻ります...',
|
||||
'Authentication successful.': '認証成功。',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'ブラウザが開かない場合は、このURLをコピーしてブラウザに貼り付けてください:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'⚠️ URL全体をコピーしてください——複数行にまたがる場合があります。',
|
||||
'Configured MCP servers:': '設定済みMCPサーバー:',
|
||||
Ready: '準備完了',
|
||||
Disconnected: '切断',
|
||||
|
|
@ -655,6 +664,8 @@ export default {
|
|||
Disable: '無効化',
|
||||
Enable: '有効化',
|
||||
Authenticate: '認証',
|
||||
'Re-authenticate': '再認証',
|
||||
'Clear Authentication': '認証をクリア',
|
||||
disabled: '無効',
|
||||
'Server:': 'サーバー:',
|
||||
Reconnect: '再接続',
|
||||
|
|
|
|||
|
|
@ -751,6 +751,15 @@ export default {
|
|||
"Falha ao autenticar com o servidor MCP '{{name}}': {{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"Redescobrindo ferramentas de '{{name}}'...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"{{count}} ferramenta(s) descoberta(s) de '{{name}}'.",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'Autenticação concluída. Retornando aos detalhes do servidor...',
|
||||
'Authentication successful.': 'Autenticação bem-sucedida.',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'Se o navegador não abrir, copie e cole esta URL no seu navegador:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'⚠️ Certifique-se de copiar a URL COMPLETA – ela pode ocupar várias linhas.',
|
||||
|
||||
// ============================================================================
|
||||
// Commands - Chat
|
||||
|
|
@ -922,6 +931,8 @@ export default {
|
|||
Disable: 'Desativar',
|
||||
Enable: 'Ativar',
|
||||
Authenticate: 'Autenticar',
|
||||
'Re-authenticate': 'Reautenticar',
|
||||
'Clear Authentication': 'Limpar autenticação',
|
||||
disabled: 'desativado',
|
||||
'Server:': 'Servidor:',
|
||||
Reconnect: 'Reconectar',
|
||||
|
|
|
|||
|
|
@ -754,6 +754,15 @@ export default {
|
|||
"Не удалось авторизоваться на MCP-сервере '{{name}}': {{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"Повторное обнаружение инструментов от '{{name}}'...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"Обнаружено {{count}} инструмент(ов) от '{{name}}'.",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'Аутентификация завершена. Возврат к деталям сервера...',
|
||||
'Authentication successful.': 'Аутентификация успешна.',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'Если браузер не открылся, скопируйте этот URL и вставьте его в браузер:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'⚠️ Убедитесь, что скопировали ПОЛНЫЙ URL — он может занимать несколько строк.',
|
||||
|
||||
// ============================================================================
|
||||
// Команды - Чат
|
||||
|
|
@ -900,6 +909,8 @@ export default {
|
|||
Disable: 'Отключить',
|
||||
Enable: 'Включить',
|
||||
Authenticate: 'Аутентификация',
|
||||
'Re-authenticate': 'Повторная аутентификация',
|
||||
'Clear Authentication': 'Очистить аутентификацию',
|
||||
disabled: 'отключен',
|
||||
'Server:': 'Сервер:',
|
||||
Reconnect: 'Переподключить',
|
||||
|
|
|
|||
|
|
@ -763,6 +763,15 @@ export default {
|
|||
"认证 MCP 服务器 '{{name}}' 失败:{{error}}",
|
||||
"Re-discovering tools from '{{name}}'...":
|
||||
"正在重新发现 '{{name}}' 的工具...",
|
||||
"Discovered {{count}} tool(s) from '{{name}}'.":
|
||||
"从 '{{name}}' 发现了 {{count}} 个工具。",
|
||||
'Authentication complete. Returning to server details...':
|
||||
'认证完成,正在返回服务器详情...',
|
||||
'Authentication successful.': '认证成功。',
|
||||
'If the browser does not open, copy and paste this URL into your browser:':
|
||||
'如果浏览器未自动打开,请复制以下 URL 并粘贴到浏览器中:',
|
||||
'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
|
||||
'⚠️ 请确保复制完整的 URL —— 它可能跨越多行。',
|
||||
|
||||
// ============================================================================
|
||||
// MCP Management Dialog
|
||||
|
|
@ -793,6 +802,8 @@ export default {
|
|||
Enable: '启用',
|
||||
Disable: '禁用',
|
||||
Authenticate: '认证',
|
||||
'Re-authenticate': '重新认证',
|
||||
'Clear Authentication': '清空认证',
|
||||
disabled: '已禁用',
|
||||
'Server:': '服务器:',
|
||||
'(disabled)': '(已禁用)',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { useConfig } from '../../contexts/ConfigContext.js';
|
|||
import {
|
||||
getMCPServerStatus,
|
||||
DiscoveredMCPTool,
|
||||
MCPOAuthTokenStorage,
|
||||
type MCPServerConfig,
|
||||
type AnyDeclarativeTool,
|
||||
type DiscoveredMCPPrompt,
|
||||
|
|
@ -109,6 +110,16 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
(t) => !t.name || !t.description,
|
||||
).length;
|
||||
|
||||
// Check if OAuth tokens exist for this server
|
||||
let hasOAuthTokens = false;
|
||||
try {
|
||||
const tokenStorage = new MCPOAuthTokenStorage();
|
||||
const credentials = await tokenStorage.getCredentials(name);
|
||||
hasOAuthTokens = credentials !== null;
|
||||
} catch {
|
||||
// Ignore errors when checking token existence
|
||||
}
|
||||
|
||||
serverInfos.push({
|
||||
name,
|
||||
status,
|
||||
|
|
@ -118,6 +129,7 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
invalidToolCount,
|
||||
promptCount: serverPrompts.length,
|
||||
isDisabled,
|
||||
hasOAuthTokens,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +261,36 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
}
|
||||
}, [fetchServerData]);
|
||||
|
||||
// Clear OAuth authentication tokens and disconnect the server
|
||||
const handleClearAuth = useCallback(async () => {
|
||||
if (!config || !selectedServer) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const tokenStorage = new MCPOAuthTokenStorage();
|
||||
await tokenStorage.deleteCredentials(selectedServer.name);
|
||||
debugLogger.info(
|
||||
`Cleared OAuth tokens for server '${selectedServer.name}'`,
|
||||
);
|
||||
|
||||
// Disconnect the server so it no longer appears as connected
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
if (toolRegistry) {
|
||||
await toolRegistry.disconnectServer(selectedServer.name);
|
||||
}
|
||||
|
||||
// Reload to update hasOAuthTokens flag and server status
|
||||
await reloadServers();
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
`Error clearing OAuth tokens for server '${selectedServer.name}':`,
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [config, selectedServer, reloadServers]);
|
||||
|
||||
// Reconnect server
|
||||
const handleReconnect = useCallback(async () => {
|
||||
if (!config || !selectedServer) return;
|
||||
|
|
@ -537,6 +579,7 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
onReconnect={handleReconnect}
|
||||
onDisable={handleDisable}
|
||||
onAuthenticate={handleAuthenticate}
|
||||
onClearAuth={handleClearAuth}
|
||||
onBack={handleNavigateBack}
|
||||
/>
|
||||
);
|
||||
|
|
@ -569,10 +612,10 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
return (
|
||||
<AuthenticateStep
|
||||
server={selectedServer}
|
||||
onSuccess={() => {
|
||||
onBack={() => {
|
||||
handleNavigateBack();
|
||||
void reloadServers();
|
||||
}}
|
||||
onBack={handleNavigateBack}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -594,6 +637,7 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
|
|||
handleReconnect,
|
||||
handleDisable,
|
||||
handleAuthenticate,
|
||||
handleClearAuth,
|
||||
handleNavigateBack,
|
||||
handleSelectTool,
|
||||
handleSelectDisableScope,
|
||||
|
|
|
|||
|
|
@ -16,13 +16,15 @@ import {
|
|||
MCPOAuthTokenStorage,
|
||||
getErrorMessage,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { OAuthDisplayPayload } from '@qwen-code/qwen-code-core';
|
||||
import { appEvents, AppEvent } from '../../../../utils/events.js';
|
||||
|
||||
type AuthState = 'idle' | 'authenticating' | 'success' | 'error';
|
||||
|
||||
const AUTO_BACK_DELAY_MS = 2000;
|
||||
|
||||
export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
||||
server,
|
||||
onSuccess,
|
||||
onBack,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
|
|
@ -39,9 +41,12 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
|||
setMessages([]);
|
||||
setErrorMessage(null);
|
||||
|
||||
// Listen for OAuth display messages (same as mcpCommand.ts)
|
||||
const displayListener = (message: string) => {
|
||||
setMessages((prev) => [...prev, message]);
|
||||
// Listen for OAuth display messages - supports both plain strings and
|
||||
// structured i18n messages ({ key, params }) emitted by the core layer.
|
||||
const displayListener = (message: OAuthDisplayPayload) => {
|
||||
const text =
|
||||
typeof message === 'string' ? message : t(message.key, message.params);
|
||||
setMessages((prev) => [...prev, text]);
|
||||
};
|
||||
appEvents.on(AppEvent.OauthDisplayMessage, displayListener);
|
||||
|
||||
|
|
@ -83,6 +88,16 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
|||
}),
|
||||
]);
|
||||
await toolRegistry.discoverToolsForServer(server.name);
|
||||
|
||||
// Show discovered tool count
|
||||
const discoveredTools = toolRegistry.getToolsByServer(server.name);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
t("Discovered {{count}} tool(s) from '{{name}}'.", {
|
||||
count: String(discoveredTools.length),
|
||||
name: server.name,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// Update the client with the new tools
|
||||
|
|
@ -91,8 +106,12 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
|||
await geminiClient.setTools();
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
t('Authentication complete. Returning to server details...'),
|
||||
]);
|
||||
|
||||
setAuthState('success');
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
setErrorMessage(getErrorMessage(error));
|
||||
setAuthState('error');
|
||||
|
|
@ -100,13 +119,22 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
|||
isRunning.current = false;
|
||||
appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener);
|
||||
}
|
||||
}, [server, config, onSuccess]);
|
||||
}, [server, config]);
|
||||
|
||||
useEffect(() => {
|
||||
runAuthentication();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Auto-navigate back after authentication succeeds
|
||||
useEffect(() => {
|
||||
if (authState !== 'success') return;
|
||||
const timer = setTimeout(() => {
|
||||
onBack();
|
||||
}, AUTO_BACK_DELAY_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [authState, onBack]);
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
|
|
@ -158,6 +186,11 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
|
|||
{t('Authenticating... Please complete the login in your browser.')}
|
||||
</Text>
|
||||
)}
|
||||
{authState === 'success' && (
|
||||
<Text color={theme.status.success}>
|
||||
{t('Authentication successful.')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ type ServerAction =
|
|||
| 'view-tools'
|
||||
| 'reconnect'
|
||||
| 'toggle-disable'
|
||||
| 'authenticate';
|
||||
| 'authenticate'
|
||||
| 'clear-auth';
|
||||
|
||||
export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
|
||||
server,
|
||||
|
|
@ -32,6 +33,7 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
|
|||
onReconnect,
|
||||
onDisable,
|
||||
onAuthenticate,
|
||||
onClearAuth,
|
||||
onBack,
|
||||
}) => {
|
||||
const statusColor = server
|
||||
|
|
@ -77,15 +79,24 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
|
|||
value: 'toggle-disable',
|
||||
});
|
||||
|
||||
// 待补充准确的认证判断方案,暂时全部开放
|
||||
// 已认证的服务器显示"重新认证",未认证的显示"认证"
|
||||
if (!server.isDisabled) {
|
||||
result.push({
|
||||
key: 'authenticate',
|
||||
label: t('Authenticate'),
|
||||
label: server.hasOAuthTokens ? t('Re-authenticate') : t('Authenticate'),
|
||||
value: 'authenticate',
|
||||
});
|
||||
}
|
||||
|
||||
// 只在存储有 OAuth 认证信息时显示“清空认证”选项
|
||||
if (!server.isDisabled && server.hasOAuthTokens) {
|
||||
result.push({
|
||||
key: 'clear-auth',
|
||||
label: t('Clear Authentication'),
|
||||
value: 'clear-auth',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [server]);
|
||||
|
||||
|
|
@ -222,6 +233,9 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
|
|||
case 'authenticate':
|
||||
onAuthenticate?.();
|
||||
break;
|
||||
case 'clear-auth':
|
||||
onClearAuth?.();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export interface MCPServerDisplayInfo {
|
|||
errorMessage?: string;
|
||||
/** 是否被禁用(在排除列表中) */
|
||||
isDisabled: boolean;
|
||||
/** 是否存储有 OAuth 认证信息 */
|
||||
hasOAuthTokens?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,6 +134,8 @@ export interface ServerDetailStepProps {
|
|||
onDisable?: () => void;
|
||||
/** OAuth 认证回调 */
|
||||
onAuthenticate?: () => void;
|
||||
/** 清空认证信息回调 */
|
||||
onClearAuth?: () => void;
|
||||
/** 返回回调 */
|
||||
onBack: () => void;
|
||||
}
|
||||
|
|
@ -178,8 +182,6 @@ export interface ToolDetailStepProps {
|
|||
export interface AuthenticateStepProps {
|
||||
/** 服务器信息 */
|
||||
server: MCPServerDisplayInfo | null;
|
||||
/** 认证成功回调 */
|
||||
onSuccess?: () => void;
|
||||
/** 返回回调 */
|
||||
onBack: () => void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue