fix(mcp): resolve OAuth callback page hanging and add i18n support (#11195)

- Fix OAuth callback server not sending HTTP response, causing browser to hang
- Add internationalization support for OAuth callback page (10 languages)
- Simplify callback page design with clean white background
- Improve user experience with localized success messages

Changes:
- src/main/services/mcp/oauth/callback.ts: Add HTTP response to OAuth callback
- src/renderer/src/i18n/: Add callback page translations for all supported languages

Signed-off-by: charles <kidccc@gmail.com>
This commit is contained in:
cheng chao 2025-11-09 01:45:25 +08:00 committed by GitHub
parent 57d9a31c0f
commit ed453750fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 141 additions and 0 deletions

View File

@ -1,4 +1,6 @@
import { loggerService } from '@logger'
import { configManager } from '@main/services/ConfigManager'
import { locales } from '@main/utils/locales'
import type EventEmitter from 'events'
import http from 'http'
import { URL } from 'url'
@ -7,6 +9,36 @@ import type { OAuthCallbackServerOptions } from './types'
const logger = loggerService.withContext('MCP:OAuthCallbackServer')
function getTranslation(key: string): string {
const language = configManager.getLanguage()
const localeData = locales[language]
if (!localeData) {
logger.warn(`No locale data found for language: ${language}`)
return key
}
const translations = localeData.translation as any
if (!translations) {
logger.warn(`No translations found for language: ${language}`)
return key
}
const keys = key.split('.')
let value = translations
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k]
} else {
logger.warn(`Translation key not found: ${key} (failed at: ${k})`)
return key // fallback to key if translation not found
}
}
return typeof value === 'string' ? value : key
}
export class CallBackServer {
private server: Promise<http.Server>
private events: EventEmitter
@ -28,6 +60,55 @@ export class CallBackServer {
if (code) {
// Emit the code event
this.events.emit('auth-code-received', code)
// Send success response to browser
const title = getTranslation('settings.mcp.oauth.callback.title')
const message = getTranslation('settings.mcp.oauth.callback.message')
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #ffffff;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
color: #2d3748;
margin: 0 0 0.5rem 0;
font-size: 24px;
font-weight: 600;
}
p {
color: #718096;
margin: 0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<p>${message}</p>
</div>
</body>
</html>
`)
} else {
res.writeHead(400, { 'Content-Type': 'text/plain' })
res.end('Missing authorization code')
}
} catch (error) {
logger.error('Error processing OAuth callback:', error as Error)

View File

@ -3863,6 +3863,12 @@
"usage": "Usage",
"version": "Version"
},
"oauth": {
"callback": {
"message": "You can close this page and return to Cherry Studio",
"title": "Authentication Successful"
}
},
"prompts": {
"arguments": "Arguments",
"availablePrompts": "Available Prompts",

View File

@ -3863,6 +3863,12 @@
"usage": "用法",
"version": "版本"
},
"oauth": {
"callback": {
"message": "您可以关闭此页面并返回 Cherry Studio",
"title": "认证成功"
}
},
"prompts": {
"arguments": "参数",
"availablePrompts": "可用提示",

View File

@ -3863,6 +3863,12 @@
"usage": "用法",
"version": "版本"
},
"oauth": {
"callback": {
"message": "您可以關閉此頁面並返回 Cherry Studio",
"title": "認證成功"
}
},
"prompts": {
"arguments": "參數",
"availablePrompts": "可用提示",

View File

@ -3863,6 +3863,12 @@
"usage": "Verwendung",
"version": "Version"
},
"oauth": {
"callback": {
"message": "Sie können diese Seite schließen und zu Cherry Studio zurückkehren",
"title": "Authentifizierung erfolgreich"
}
},
"prompts": {
"arguments": "Parameter",
"availablePrompts": "Verfügbare Prompts",

View File

@ -3863,6 +3863,12 @@
"usage": "Χρήση",
"version": "Έκδοση"
},
"oauth": {
"callback": {
"message": "Μπορείτε να κλείσετε αυτήν τη σελίδα και να επιστρέψετε στο Cherry Studio",
"title": "Επιτυχής Ταυτοποίηση"
}
},
"prompts": {
"arguments": "Ορίσματα",
"availablePrompts": "Διαθέσιμες Υποδείξεις",

View File

@ -3863,6 +3863,12 @@
"usage": "Uso",
"version": "Versión"
},
"oauth": {
"callback": {
"message": "Puede cerrar esta página y volver a Cherry Studio",
"title": "Autenticación Exitosa"
}
},
"prompts": {
"arguments": "Argumentos",
"availablePrompts": "Indicaciones disponibles",

View File

@ -3863,6 +3863,12 @@
"usage": "Utilisation",
"version": "Version"
},
"oauth": {
"callback": {
"message": "Vous pouvez fermer cette page et retourner à Cherry Studio",
"title": "Authentification Réussie"
}
},
"prompts": {
"arguments": "Arguments",
"availablePrompts": "Invites disponibles",

View File

@ -3863,6 +3863,12 @@
"usage": "使用法",
"version": "バージョン"
},
"oauth": {
"callback": {
"message": "このページを閉じてCherry Studioに戻ることができます",
"title": "認証成功"
}
},
"prompts": {
"arguments": "引数",
"availablePrompts": "利用可能なプロンプト",

View File

@ -3863,6 +3863,12 @@
"usage": "Uso",
"version": "Versão"
},
"oauth": {
"callback": {
"message": "Você pode fechar esta página e retornar ao Cherry Studio",
"title": "Autenticação Bem-Sucedida"
}
},
"prompts": {
"arguments": "Argumentos",
"availablePrompts": "Dicas disponíveis",

View File

@ -3863,6 +3863,12 @@
"usage": "Использование",
"version": "Версия"
},
"oauth": {
"callback": {
"message": "Вы можете закрыть эту страницу и вернуться в Cherry Studio",
"title": "Аутентификация Успешна"
}
},
"prompts": {
"arguments": "Аргументы",
"availablePrompts": "Доступные подсказки",