+
{tx("settings.mcp.configImport", "Import mcp.json")}
@@ -8295,7 +8908,42 @@ function McpCustomServerPanel({
);
}
-function mcpPresetStatusLabel(status: string, tx: (key: string, fallback: string) => string): string {
+function mcpOAuthStatusText(
+ status: McpOAuthFlowPayload["status"],
+ popupBlocked: boolean,
+ tx: (key: string, fallback: string) => string,
+ completionInput?: McpOAuthFlowPayload["completion_input"],
+): string {
+ switch (status) {
+ case "starting":
+ return tx("settings.mcp.preparingSignIn", "Preparing secure sign-in...");
+ case "authorization_required":
+ if (completionInput === "callback_url") {
+ return tx(
+ "settings.mcp.manualCallbackRequired",
+ "Finish signing in, then paste the callback URL into nanobot.",
+ );
+ }
+ return popupBlocked
+ ? tx("settings.mcp.openSignInToContinue", "Open the sign-in page to continue.")
+ : tx("settings.mcp.finishSignInInBrowser", "Finish signing in in the browser window.");
+ case "connecting":
+ return tx("settings.mcp.finishingConnection", "Finishing connection...");
+ case "authorized":
+ return tx("settings.mcp.activatingTools", "Activating tools...");
+ case "connected":
+ return tx("settings.mcp.connected", "Connected.");
+ case "failed":
+ return tx("settings.mcp.connectionFailed", "Connection failed.");
+ case "cancelled":
+ return tx("settings.mcp.connectionCancelled", "Connection cancelled.");
+ }
+}
+
+function mcpPresetStatusLabel(
+ status: string,
+ tx: (key: string, fallback: string) => string,
+): string {
switch (status) {
case "configured":
return tx("settings.mcp.statusConfigured", "Configured");
diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json
index b0b5637a0..6339ff56b 100644
--- a/webui/src/i18n/locales/en/common.json
+++ b/webui/src/i18n/locales/en/common.json
@@ -319,8 +319,8 @@
"filterInstalled": "Enabled",
"filterNotInstalled": "Not enabled",
"searchPlaceholder": "Search MCP presets",
- "moreOptions": "Add integration",
- "moreOptionsSubtitle": "Connect a custom tool server or import an existing configuration.",
+ "moreOptions": "Add MCP server",
+ "moreOptionsSubtitle": "Connect a custom MCP server or import an existing configuration.",
"customTitle": "Custom MCP",
"customSubtitle": "Add any stdio, HTTP, or SSE MCP server.",
"customAction": "Custom",
@@ -328,9 +328,14 @@
"serverName": "Server name",
"serverUrl": "URL",
"transport": "Transport",
+ "authentication": "Authentication",
+ "authNone": "None",
+ "authHeaders": "Headers",
"command": "Command",
"args": "Args JSON",
"headers": "Headers JSON",
+ "oauthAfterSave": "Save the server, then select Connect to sign in.",
+ "headersHelp": "Add the request headers used by this server.",
"env": "Env JSON",
"timeout": "Tool timeout",
"advancedOptions": "Advanced options",
@@ -357,6 +362,21 @@
"keepExisting": "Leave blank to keep existing",
"statusConfigured": "Configured",
"statusMissingCredentials": "Needs key",
+ "connectingAccount": "Connecting {{name}}",
+ "connectingLabel": "Connecting…",
+ "continueSignIn": "Continue sign-in",
+ "preparingSignIn": "Preparing secure sign-in…",
+ "openSignInToContinue": "Open the sign-in page to continue.",
+ "finishSignInInBrowser": "Finish signing in in the browser window.",
+ "manualCallbackRequired": "Finish signing in, then paste the callback URL into nanobot.",
+ "manualCallbackHelp": "After approving access, the localhost page will not load. Copy its full URL from the address bar and paste it here.",
+ "finishingConnection": "Finishing connection…",
+ "activatingTools": "Activating tools…",
+ "connected": "Connected.",
+ "connectionFailed": "Connection failed.",
+ "connectionCancelled": "Connection cancelled.",
+ "reloadFailed": "Signed in, but nanobot could not connect the tools. Try restarting nanobot.",
+ "oauthFailed": "Unable to connect. Try signing in again.",
"statusMissingDependency": "Needs dependency",
"statusComingSoon": "Coming soon",
"comingSoon": "Coming soon",
@@ -584,27 +604,28 @@
"apps": {
"description": "Add tools to nanobot, then @ them in chat.",
"cliLabel": "App",
- "mcpLabel": "Integration",
+ "mcpLabel": "MCP",
"channelLabel": "Channel",
"featureLabel": "Feature",
"filterAll": "Ready",
"filterPlugins": "Plugins",
"filterCli": "Apps",
- "filterMcp": "Integrations",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} ready",
- "caption": "{{cli}} apps · {{mcp}} integrations",
+ "caption": "{{cli}} apps · {{mcp}} MCP tools",
"searchPlaceholder": "Search tools",
"featured": "Tools",
+ "mcpTools": "MCP tools",
"loading": "Loading Apps...",
"empty": "No tools match your search.",
"emptyApps": "No apps available.",
- "emptyIntegrations": "No integrations available.",
+ "emptyIntegrations": "No MCP tools available.",
"emptyReady": "No tools are ready yet.",
"clearSearch": "Clear search",
"browseApps": "Browse apps",
- "browseIntegrations": "Browse integrations",
- "emptyIntegrationsHint": "Add a custom integration below.",
- "restartRequired": "Restart nanobot to apply updated apps and integrations."
+ "browseIntegrations": "Browse MCP tools",
+ "emptyIntegrationsHint": "Add a custom MCP server below.",
+ "restartRequired": "Restart nanobot to apply updated apps and MCP tools."
},
"channels": {
"description": "Connect chat apps, email, and WebUI to nanobot.",
diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json
index ae2e9fb3e..09484c1bd 100644
--- a/webui/src/i18n/locales/es/common.json
+++ b/webui/src/i18n/locales/es/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "Habilitados",
"filterNotInstalled": "No habilitados",
"searchPlaceholder": "Buscar preajustes MCP",
- "moreOptions": "Más opciones de MCP",
- "moreOptionsSubtitle": "Añade un servidor personalizado o importa mcp.json.",
+ "moreOptions": "Añadir servidor MCP",
+ "moreOptionsSubtitle": "Conecta un servidor MCP personalizado o importa una configuración existente.",
"customTitle": "MCP personalizado",
"customSubtitle": "Añade cualquier servidor MCP stdio, HTTP o SSE.",
"customAction": "Personalizado",
@@ -513,9 +513,14 @@
"serverName": "Nombre del servidor",
"serverUrl": "URL",
"transport": "Transporte",
+ "authentication": "Autenticación",
+ "authNone": "Ninguna",
+ "authHeaders": "Encabezados",
"command": "Comando",
"args": "Argumentos JSON",
"headers": "Encabezados JSON",
+ "oauthAfterSave": "Guarda el servidor y selecciona Conectar para iniciar sesión.",
+ "headersHelp": "Añade los encabezados de solicitud que utiliza este servidor.",
"env": "Entorno JSON",
"timeout": "Tiempo límite de herramienta",
"advancedOptions": "Opciones avanzadas",
@@ -542,6 +547,21 @@
"keepExisting": "Déjalo en blanco para conservar el valor actual",
"statusConfigured": "Configurado",
"statusMissingCredentials": "Necesita clave",
+ "connectingAccount": "Conectando {{name}}",
+ "connectingLabel": "Conectando…",
+ "continueSignIn": "Continuar inicio de sesión",
+ "preparingSignIn": "Preparando un inicio de sesión seguro…",
+ "openSignInToContinue": "Abre la página de inicio de sesión para continuar.",
+ "finishSignInInBrowser": "Termina de iniciar sesión en la ventana del navegador.",
+ "manualCallbackRequired": "Termina de iniciar sesión y pega la URL de devolución en nanobot.",
+ "manualCallbackHelp": "Después de aprobar el acceso, la página de localhost no se cargará. Copia la URL completa de la barra de direcciones y pégala aquí.",
+ "finishingConnection": "Finalizando la conexión…",
+ "activatingTools": "Activando herramientas…",
+ "connected": "Conectado.",
+ "connectionFailed": "Error de conexión.",
+ "connectionCancelled": "Conexión cancelada.",
+ "reloadFailed": "Has iniciado sesión, pero nanobot no pudo conectar las herramientas. Prueba a reiniciar nanobot.",
+ "oauthFailed": "No se pudo conectar. Intenta iniciar sesión de nuevo.",
"statusMissingDependency": "Necesita dependencia",
"statusComingSoon": "Próximamente",
"comingSoon": "Próximamente",
@@ -571,27 +591,28 @@
"apps": {
"description": "Añade herramientas a nanobot y luego úsalas con @ en el chat.",
"cliLabel": "Aplicación",
- "mcpLabel": "Integración",
+ "mcpLabel": "MCP",
"channelLabel": "Canal",
"featureLabel": "Función",
"filterAll": "Listo",
"filterPlugins": "Complementos",
"filterCli": "Aplicaciones",
- "filterMcp": "Integraciones",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} listos",
- "caption": "{{cli}} aplicaciones · {{mcp}} integraciones",
+ "caption": "{{cli}} aplicaciones · {{mcp}} herramientas MCP",
"searchPlaceholder": "Buscar aplicaciones",
"featured": "Herramientas",
+ "mcpTools": "Herramientas MCP",
"loading": "Cargando aplicaciones...",
"empty": "Ninguna herramienta coincide con tu búsqueda.",
"emptyApps": "No hay aplicaciones disponibles.",
- "emptyIntegrations": "No hay integraciones disponibles.",
+ "emptyIntegrations": "No hay herramientas MCP disponibles.",
"emptyReady": "Todavía no hay herramientas listas.",
"clearSearch": "Borrar búsqueda",
"browseApps": "Explorar aplicaciones",
- "browseIntegrations": "Explorar integraciones",
- "emptyIntegrationsHint": "Añade una integración personalizada abajo.",
- "restartRequired": "Reinicia nanobot para aplicar las aplicaciones y funciones actualizadas."
+ "browseIntegrations": "Explorar herramientas MCP",
+ "emptyIntegrationsHint": "Añade un servidor MCP personalizado abajo.",
+ "restartRequired": "Reinicia nanobot para aplicar las aplicaciones y herramientas MCP actualizadas."
},
"channels": {
"description": "Conecta nanobot con aplicaciones de chat. Instalar el soporte solo añade el paquete de integración; la mayoría de canales aún necesitan tokens o configuración del espacio de trabajo.",
diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json
index aa32089c6..4eff41b0f 100644
--- a/webui/src/i18n/locales/fr/common.json
+++ b/webui/src/i18n/locales/fr/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "Activés",
"filterNotInstalled": "Non activés",
"searchPlaceholder": "Rechercher des préréglages MCP",
- "moreOptions": "Plus d'options MCP",
- "moreOptionsSubtitle": "Ajoutez un serveur personnalisé ou importez mcp.json.",
+ "moreOptions": "Ajouter un serveur MCP",
+ "moreOptionsSubtitle": "Connectez un serveur MCP personnalisé ou importez une configuration existante.",
"customTitle": "MCP personnalisé",
"customSubtitle": "Ajoutez n'importe quel serveur MCP stdio, HTTP ou SSE.",
"customAction": "Personnalisé",
@@ -513,9 +513,14 @@
"serverName": "Nom du serveur",
"serverUrl": "URL",
"transport": "Transport",
+ "authentication": "Authentification",
+ "authNone": "Aucune",
+ "authHeaders": "En-têtes",
"command": "Commande",
"args": "Arguments JSON",
"headers": "En-têtes JSON",
+ "oauthAfterSave": "Enregistrez le serveur, puis sélectionnez Se connecter pour vous identifier.",
+ "headersHelp": "Ajoutez les en-têtes de requête utilisés par ce serveur.",
"env": "Environnement JSON",
"timeout": "Délai d'outil",
"advancedOptions": "Options avancées",
@@ -542,6 +547,21 @@
"keepExisting": "Laissez vide pour conserver la valeur actuelle",
"statusConfigured": "Configuré",
"statusMissingCredentials": "Clé requise",
+ "connectingAccount": "Connexion à {{name}}",
+ "connectingLabel": "Connexion…",
+ "continueSignIn": "Continuer la connexion",
+ "preparingSignIn": "Préparation d’une connexion sécurisée…",
+ "openSignInToContinue": "Ouvrez la page de connexion pour continuer.",
+ "finishSignInInBrowser": "Terminez la connexion dans la fenêtre du navigateur.",
+ "manualCallbackRequired": "Terminez la connexion, puis collez l’URL de rappel dans nanobot.",
+ "manualCallbackHelp": "Après avoir autorisé l’accès, la page localhost ne se chargera pas. Copiez son URL complète depuis la barre d’adresse et collez-la ici.",
+ "finishingConnection": "Finalisation de la connexion…",
+ "activatingTools": "Activation des outils…",
+ "connected": "Connecté.",
+ "connectionFailed": "Échec de la connexion.",
+ "connectionCancelled": "Connexion annulée.",
+ "reloadFailed": "Connexion réussie, mais nanobot n’a pas pu activer les outils. Essayez de redémarrer nanobot.",
+ "oauthFailed": "Connexion impossible. Essayez de vous reconnecter.",
"statusMissingDependency": "Dépendance requise",
"statusComingSoon": "Bientôt disponible",
"comingSoon": "Bientôt disponible",
@@ -570,27 +590,28 @@
"apps": {
"description": "Ajoutez des outils à nanobot, puis utilisez-les avec @ dans le chat.",
"cliLabel": "Application",
- "mcpLabel": "Intégration",
+ "mcpLabel": "MCP",
"channelLabel": "Canal",
"featureLabel": "Fonction",
"filterAll": "Prêts",
"filterPlugins": "Extensions",
"filterCli": "Applications",
- "filterMcp": "Intégrations",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} prêts",
- "caption": "{{cli}} applications · {{mcp}} intégrations",
+ "caption": "{{cli}} applications · {{mcp}} outils MCP",
"searchPlaceholder": "Rechercher des applications",
"featured": "Outils",
+ "mcpTools": "Outils MCP",
"loading": "Chargement des applications...",
"empty": "Aucun outil ne correspond à votre recherche.",
"emptyApps": "Aucune application disponible.",
- "emptyIntegrations": "Aucune intégration disponible.",
+ "emptyIntegrations": "Aucun outil MCP disponible.",
"emptyReady": "Aucun outil n’est encore prêt.",
"clearSearch": "Effacer la recherche",
"browseApps": "Parcourir les applications",
- "browseIntegrations": "Parcourir les intégrations",
- "emptyIntegrationsHint": "Ajoutez une intégration personnalisée ci-dessous.",
- "restartRequired": "Redémarrez nanobot pour appliquer les applications et fonctions mises à jour."
+ "browseIntegrations": "Parcourir les outils MCP",
+ "emptyIntegrationsHint": "Ajoutez un serveur MCP personnalisé ci-dessous.",
+ "restartRequired": "Redémarrez nanobot pour appliquer les applications et outils MCP mis à jour."
},
"channels": {
"description": "Connectez nanobot aux applications de discussion. L'installation du support ajoute seulement le paquet d'intégration ; la plupart des canaux nécessitent encore des jetons ou des réglages d'espace de travail.",
diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json
index 81326edf9..56231fe22 100644
--- a/webui/src/i18n/locales/id/common.json
+++ b/webui/src/i18n/locales/id/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "Aktif",
"filterNotInstalled": "Tidak aktif",
"searchPlaceholder": "Cari prasetel MCP",
- "moreOptions": "Opsi MCP lainnya",
- "moreOptionsSubtitle": "Tambahkan server khusus atau impor mcp.json.",
+ "moreOptions": "Tambahkan server MCP",
+ "moreOptionsSubtitle": "Hubungkan server MCP khusus atau impor konfigurasi yang ada.",
"customTitle": "MCP khusus",
"customSubtitle": "Tambahkan server MCP stdio, HTTP, atau SSE apa pun.",
"customAction": "Khusus",
@@ -513,9 +513,14 @@
"serverName": "Nama server",
"serverUrl": "URL",
"transport": "Transport",
+ "authentication": "Autentikasi",
+ "authNone": "Tidak ada",
+ "authHeaders": "Header",
"command": "Perintah",
"args": "Argumen JSON",
"headers": "Header JSON",
+ "oauthAfterSave": "Simpan server, lalu pilih Hubungkan untuk masuk.",
+ "headersHelp": "Tambahkan header permintaan yang digunakan server ini.",
"env": "Lingkungan JSON",
"timeout": "Batas waktu alat",
"advancedOptions": "Opsi lanjutan",
@@ -542,6 +547,21 @@
"keepExisting": "Biarkan kosong untuk mempertahankan nilai saat ini",
"statusConfigured": "Terkonfigurasi",
"statusMissingCredentials": "Butuh kunci",
+ "connectingAccount": "Menghubungkan {{name}}",
+ "connectingLabel": "Menghubungkan…",
+ "continueSignIn": "Lanjutkan masuk",
+ "preparingSignIn": "Menyiapkan proses masuk yang aman…",
+ "openSignInToContinue": "Buka halaman masuk untuk melanjutkan.",
+ "finishSignInInBrowser": "Selesaikan proses masuk di jendela browser.",
+ "manualCallbackRequired": "Selesaikan proses masuk, lalu tempel URL callback ke nanobot.",
+ "manualCallbackHelp": "Setelah menyetujui akses, halaman localhost tidak akan terbuka. Salin URL lengkap dari bilah alamat lalu tempel di sini.",
+ "finishingConnection": "Menyelesaikan koneksi…",
+ "activatingTools": "Mengaktifkan alat…",
+ "connected": "Terhubung.",
+ "connectionFailed": "Koneksi gagal.",
+ "connectionCancelled": "Koneksi dibatalkan.",
+ "reloadFailed": "Anda sudah masuk, tetapi nanobot tidak dapat menghubungkan alat. Coba mulai ulang nanobot.",
+ "oauthFailed": "Tidak dapat terhubung. Coba masuk lagi.",
"statusMissingDependency": "Butuh dependensi",
"statusComingSoon": "Segera hadir",
"comingSoon": "Segera hadir",
@@ -570,27 +590,28 @@
"apps": {
"description": "Tambahkan alat ke nanobot, lalu gunakan dengan @ di chat.",
"cliLabel": "Aplikasi",
- "mcpLabel": "Integrasi",
+ "mcpLabel": "MCP",
"channelLabel": "Kanal",
"featureLabel": "Fitur",
"filterAll": "Siap",
"filterPlugins": "Plugin",
"filterCli": "Aplikasi",
- "filterMcp": "Integrasi",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} siap",
- "caption": "{{cli}} aplikasi · {{mcp}} integrasi",
+ "caption": "{{cli}} aplikasi · {{mcp}} alat MCP",
"searchPlaceholder": "Cari aplikasi",
"featured": "Alat",
+ "mcpTools": "Alat MCP",
"loading": "Memuat aplikasi...",
"empty": "Tidak ada alat yang cocok dengan pencarian Anda.",
"emptyApps": "Tidak ada aplikasi yang tersedia.",
- "emptyIntegrations": "Tidak ada integrasi yang tersedia.",
+ "emptyIntegrations": "Tidak ada alat MCP yang tersedia.",
"emptyReady": "Belum ada alat yang siap.",
"clearSearch": "Hapus pencarian",
"browseApps": "Jelajahi aplikasi",
- "browseIntegrations": "Jelajahi integrasi",
- "emptyIntegrationsHint": "Tambahkan integrasi khusus di bawah.",
- "restartRequired": "Mulai ulang nanobot untuk menerapkan aplikasi dan fitur yang diperbarui."
+ "browseIntegrations": "Jelajahi alat MCP",
+ "emptyIntegrationsHint": "Tambahkan server MCP khusus di bawah.",
+ "restartRequired": "Mulai ulang nanobot untuk menerapkan aplikasi dan alat MCP yang diperbarui."
},
"channels": {
"description": "Hubungkan nanobot ke aplikasi chat. Memasang dukungan hanya menambahkan paket integrasi; sebagian besar kanal tetap memerlukan token atau pengaturan ruang kerja.",
diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json
index 08fe99bd7..f9e60db06 100644
--- a/webui/src/i18n/locales/ja/common.json
+++ b/webui/src/i18n/locales/ja/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "有効",
"filterNotInstalled": "未有効",
"searchPlaceholder": "MCP プリセットを検索",
- "moreOptions": "その他の MCP オプション",
- "moreOptionsSubtitle": "カスタムサーバーを追加するか mcp.json をインポートします。",
+ "moreOptions": "MCP サーバーを追加",
+ "moreOptionsSubtitle": "カスタム MCP サーバーを接続するか、既存の設定をインポートします。",
"customTitle": "カスタム MCP",
"customSubtitle": "任意の stdio、HTTP、SSE MCP サーバーを追加します。",
"customAction": "カスタム",
@@ -513,9 +513,14 @@
"serverName": "サーバー名",
"serverUrl": "URL",
"transport": "トランスポート",
+ "authentication": "認証",
+ "authNone": "なし",
+ "authHeaders": "ヘッダー",
"command": "コマンド",
"args": "引数 JSON",
"headers": "ヘッダー JSON",
+ "oauthAfterSave": "サーバーを保存してから、[接続]を選択してサインインします。",
+ "headersHelp": "このサーバーで使用するリクエストヘッダーを追加します。",
"env": "環境変数 JSON",
"timeout": "ツールのタイムアウト",
"advancedOptions": "詳細オプション",
@@ -542,6 +547,21 @@
"keepExisting": "既存の値を維持するには空欄のままにします",
"statusConfigured": "設定済み",
"statusMissingCredentials": "キーが必要",
+ "connectingAccount": "{{name}} に接続しています",
+ "connectingLabel": "接続中…",
+ "continueSignIn": "サインインを続ける",
+ "preparingSignIn": "安全なサインインを準備しています…",
+ "openSignInToContinue": "サインインページを開いて続行してください。",
+ "finishSignInInBrowser": "ブラウザウィンドウでサインインを完了してください。",
+ "manualCallbackRequired": "サインインを完了し、コールバック URL を nanobot に貼り付けてください。",
+ "manualCallbackHelp": "アクセスを承認すると localhost ページは開きません。アドレスバーから完全な URL をコピーして、ここに貼り付けてください。",
+ "finishingConnection": "接続を完了しています…",
+ "activatingTools": "ツールを有効にしています…",
+ "connected": "接続しました。",
+ "connectionFailed": "接続に失敗しました。",
+ "connectionCancelled": "接続をキャンセルしました。",
+ "reloadFailed": "サインインしましたが、nanobot はツールに接続できませんでした。nanobot を再起動してください。",
+ "oauthFailed": "接続できません。もう一度サインインしてください。",
"statusMissingDependency": "依存関係が必要",
"statusComingSoon": "近日公開",
"comingSoon": "近日公開",
@@ -570,27 +590,28 @@
"apps": {
"description": "nanobot にツールを追加し、チャットで @ を付けて使用できます。",
"cliLabel": "アプリ",
- "mcpLabel": "連携",
+ "mcpLabel": "MCP",
"channelLabel": "チャンネル",
"featureLabel": "機能",
"filterAll": "使用可能",
"filterPlugins": "プラグイン",
"filterCli": "アプリ",
- "filterMcp": "連携",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} 件使用可能",
- "caption": "アプリ {{cli}} 件 · 連携 {{mcp}} 件",
+ "caption": "アプリ {{cli}} 件 · MCP ツール {{mcp}} 件",
"searchPlaceholder": "アプリを検索",
"featured": "ツール",
+ "mcpTools": "MCP ツール",
"loading": "アプリを読み込み中...",
"empty": "検索条件に一致するツールはありません。",
"emptyApps": "利用できるアプリはありません。",
- "emptyIntegrations": "利用できる連携はありません。",
+ "emptyIntegrations": "利用できる MCP ツールはありません。",
"emptyReady": "使用可能なツールはまだありません。",
"clearSearch": "検索をクリア",
"browseApps": "アプリを見る",
- "browseIntegrations": "連携を見る",
- "emptyIntegrationsHint": "下からカスタム連携を追加できます。",
- "restartRequired": "更新したアプリと機能を反映するには nanobot を再起動してください。"
+ "browseIntegrations": "MCP ツールを見る",
+ "emptyIntegrationsHint": "下からカスタム MCP サーバーを追加できます。",
+ "restartRequired": "更新したアプリと MCP ツールを反映するには nanobot を再起動してください。"
},
"channels": {
"description": "nanobot をチャットアプリに接続します。サポートのインストールは統合パッケージを追加するだけで、多くのチャンネルでは引き続きトークンやワークスペース設定が必要です。",
diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json
index dd3bfdf60..511d6f142 100644
--- a/webui/src/i18n/locales/ko/common.json
+++ b/webui/src/i18n/locales/ko/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "활성화됨",
"filterNotInstalled": "비활성",
"searchPlaceholder": "MCP 프리셋 검색",
- "moreOptions": "추가 MCP 옵션",
- "moreOptionsSubtitle": "사용자 지정 서버를 추가하거나 mcp.json을 가져옵니다.",
+ "moreOptions": "MCP 서버 추가",
+ "moreOptionsSubtitle": "사용자 지정 MCP 서버를 연결하거나 기존 구성을 가져옵니다.",
"customTitle": "사용자 지정 MCP",
"customSubtitle": "stdio, HTTP 또는 SSE MCP 서버를 추가합니다.",
"customAction": "사용자 지정",
@@ -513,9 +513,14 @@
"serverName": "서버 이름",
"serverUrl": "URL",
"transport": "전송 방식",
+ "authentication": "인증",
+ "authNone": "없음",
+ "authHeaders": "헤더",
"command": "명령",
"args": "인자 JSON",
"headers": "헤더 JSON",
+ "oauthAfterSave": "서버를 저장한 다음 연결을 선택하여 로그인하세요.",
+ "headersHelp": "이 서버에서 사용하는 요청 헤더를 추가하세요.",
"env": "환경 변수 JSON",
"timeout": "도구 제한 시간",
"advancedOptions": "고급 옵션",
@@ -542,6 +547,21 @@
"keepExisting": "기존 값을 유지하려면 비워 두세요",
"statusConfigured": "구성됨",
"statusMissingCredentials": "키 필요",
+ "connectingAccount": "{{name}} 연결 중",
+ "connectingLabel": "연결 중…",
+ "continueSignIn": "로그인 계속",
+ "preparingSignIn": "안전한 로그인을 준비하는 중…",
+ "openSignInToContinue": "계속하려면 로그인 페이지를 여세요.",
+ "finishSignInInBrowser": "브라우저 창에서 로그인을 완료하세요.",
+ "manualCallbackRequired": "로그인을 완료한 다음 콜백 URL을 nanobot에 붙여 넣으세요.",
+ "manualCallbackHelp": "접근을 승인하면 localhost 페이지가 열리지 않습니다. 주소 표시줄에서 전체 URL을 복사해 여기에 붙여 넣으세요.",
+ "finishingConnection": "연결을 마무리하는 중…",
+ "activatingTools": "도구를 활성화하는 중…",
+ "connected": "연결됨.",
+ "connectionFailed": "연결에 실패했습니다.",
+ "connectionCancelled": "연결을 취소했습니다.",
+ "reloadFailed": "로그인했지만 nanobot에서 도구를 연결하지 못했습니다. nanobot을 다시 시작해 보세요.",
+ "oauthFailed": "연결할 수 없습니다. 다시 로그인해 보세요.",
"statusMissingDependency": "의존성 필요",
"statusComingSoon": "곧 제공",
"comingSoon": "곧 제공",
@@ -570,27 +590,28 @@
"apps": {
"description": "nanobot에 도구를 추가한 뒤 채팅에서 @로 사용하세요.",
"cliLabel": "앱",
- "mcpLabel": "연동",
+ "mcpLabel": "MCP",
"channelLabel": "채널",
"featureLabel": "기능",
"filterAll": "사용 가능",
"filterPlugins": "플러그인",
"filterCli": "앱",
- "filterMcp": "연동",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}}개 사용 가능",
- "caption": "앱 {{cli}}개 · 연동 {{mcp}}개",
+ "caption": "앱 {{cli}}개 · MCP 도구 {{mcp}}개",
"searchPlaceholder": "앱 검색",
"featured": "도구",
+ "mcpTools": "MCP 도구",
"loading": "앱을 불러오는 중...",
"empty": "검색과 일치하는 도구가 없습니다.",
"emptyApps": "사용 가능한 앱이 없습니다.",
- "emptyIntegrations": "사용 가능한 연동이 없습니다.",
+ "emptyIntegrations": "사용 가능한 MCP 도구가 없습니다.",
"emptyReady": "아직 준비된 도구가 없습니다.",
"clearSearch": "검색 지우기",
"browseApps": "앱 둘러보기",
- "browseIntegrations": "연동 둘러보기",
- "emptyIntegrationsHint": "아래에서 사용자 지정 연동을 추가하세요.",
- "restartRequired": "업데이트된 앱과 기능을 적용하려면 nanobot을 다시 시작하세요."
+ "browseIntegrations": "MCP 도구 둘러보기",
+ "emptyIntegrationsHint": "아래에서 사용자 지정 MCP 서버를 추가하세요.",
+ "restartRequired": "업데이트된 앱과 MCP 도구를 적용하려면 nanobot을 다시 시작하세요."
},
"channels": {
"description": "nanobot을 채팅 앱에 연결합니다. 지원 설치는 통합 패키지만 추가하며, 대부분의 채널은 여전히 토큰이나 워크스페이스 설정이 필요합니다.",
diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json
index eef0bea72..7d9d4e191 100644
--- a/webui/src/i18n/locales/pt-BR/common.json
+++ b/webui/src/i18n/locales/pt-BR/common.json
@@ -319,8 +319,8 @@
"filterInstalled": "Habilitadas",
"filterNotInstalled": "Não habilitadas",
"searchPlaceholder": "Buscar predefinições MCP",
- "moreOptions": "Adicionar integração",
- "moreOptionsSubtitle": "Conecte um servidor de ferramentas personalizado ou importe uma configuração existente.",
+ "moreOptions": "Adicionar servidor MCP",
+ "moreOptionsSubtitle": "Conecte um servidor MCP personalizado ou importe uma configuração existente.",
"customTitle": "MCP personalizado",
"customSubtitle": "Adicione qualquer servidor MCP stdio, HTTP ou SSE.",
"customAction": "Personalizado",
@@ -328,9 +328,14 @@
"serverName": "Nome do servidor",
"serverUrl": "URL",
"transport": "Transporte",
+ "authentication": "Autenticação",
+ "authNone": "Nenhuma",
+ "authHeaders": "Cabeçalhos",
"command": "Comando",
"args": "Argumentos JSON",
"headers": "Cabeçalhos JSON",
+ "oauthAfterSave": "Salve o servidor e selecione Conectar para entrar.",
+ "headersHelp": "Adicione os cabeçalhos de solicitação usados por este servidor.",
"env": "Ambiente JSON",
"timeout": "Tempo limite da ferramenta",
"advancedOptions": "Opções avançadas",
@@ -357,6 +362,21 @@
"keepExisting": "Deixe em branco para manter o valor atual",
"statusConfigured": "Configurado",
"statusMissingCredentials": "Precisa de chave",
+ "connectingAccount": "Conectando {{name}}",
+ "connectingLabel": "Conectando…",
+ "continueSignIn": "Continuar login",
+ "preparingSignIn": "Preparando login seguro…",
+ "openSignInToContinue": "Abra a página de login para continuar.",
+ "finishSignInInBrowser": "Conclua o login na janela do navegador.",
+ "manualCallbackRequired": "Conclua o login e cole a URL de callback no nanobot.",
+ "manualCallbackHelp": "Depois de autorizar o acesso, a página localhost não será carregada. Copie a URL completa da barra de endereços e cole-a aqui.",
+ "finishingConnection": "Finalizando conexão…",
+ "activatingTools": "Ativando ferramentas…",
+ "connected": "Conectado.",
+ "connectionFailed": "Falha na conexão.",
+ "connectionCancelled": "Conexão cancelada.",
+ "reloadFailed": "Login concluído, mas o nanobot não conseguiu conectar as ferramentas. Tente reiniciar o nanobot.",
+ "oauthFailed": "Não foi possível conectar. Tente fazer login novamente.",
"statusMissingDependency": "Precisa de dependência",
"statusComingSoon": "Em breve",
"comingSoon": "Em breve",
@@ -584,27 +604,28 @@
"apps": {
"description": "Adicione ferramentas ao nanobot e mencione-as com @ na conversa.",
"cliLabel": "Aplicativo",
- "mcpLabel": "Integração",
+ "mcpLabel": "MCP",
"channelLabel": "Canal",
"featureLabel": "Recurso",
"filterAll": "Prontos",
"filterPlugins": "Complementos",
"filterCli": "Aplicativos",
- "filterMcp": "Integrações",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} prontos",
- "caption": "{{cli}} aplicativos · {{mcp}} integrações",
+ "caption": "{{cli}} aplicativos · {{mcp}} ferramentas MCP",
"searchPlaceholder": "Buscar ferramentas",
"featured": "Ferramentas",
+ "mcpTools": "Ferramentas MCP",
"loading": "Carregando aplicativos...",
"empty": "Nenhuma ferramenta corresponde à sua busca.",
"emptyApps": "Nenhum aplicativo disponível.",
- "emptyIntegrations": "Nenhuma integração disponível.",
+ "emptyIntegrations": "Nenhuma ferramenta MCP disponível.",
"emptyReady": "Ainda não há ferramentas prontas.",
"clearSearch": "Limpar busca",
"browseApps": "Explorar aplicativos",
- "browseIntegrations": "Explorar integrações",
- "emptyIntegrationsHint": "Adicione uma integração personalizada abaixo.",
- "restartRequired": "Reinicie o nanobot para aplicar os aplicativos e integrações atualizados."
+ "browseIntegrations": "Explorar ferramentas MCP",
+ "emptyIntegrationsHint": "Adicione um servidor MCP personalizado abaixo.",
+ "restartRequired": "Reinicie o nanobot para aplicar os aplicativos e ferramentas MCP atualizados."
},
"channels": {
"description": "Conecte aplicativos de chat, e-mail e WebUI ao nanobot.",
diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json
index 98ca6c1b3..6e3a99a4f 100644
--- a/webui/src/i18n/locales/vi/common.json
+++ b/webui/src/i18n/locales/vi/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "Đã bật",
"filterNotInstalled": "Chưa bật",
"searchPlaceholder": "Tìm cấu hình đặt trước MCP",
- "moreOptions": "Tùy chọn MCP khác",
- "moreOptionsSubtitle": "Thêm máy chủ tùy chỉnh hoặc nhập mcp.json.",
+ "moreOptions": "Thêm máy chủ MCP",
+ "moreOptionsSubtitle": "Kết nối máy chủ MCP tùy chỉnh hoặc nhập cấu hình hiện có.",
"customTitle": "MCP tùy chỉnh",
"customSubtitle": "Thêm bất kỳ máy chủ MCP stdio, HTTP hoặc SSE nào.",
"customAction": "Tùy chỉnh",
@@ -513,9 +513,14 @@
"serverName": "Tên máy chủ",
"serverUrl": "URL",
"transport": "Giao thức truyền",
+ "authentication": "Xác thực",
+ "authNone": "Không có",
+ "authHeaders": "Header",
"command": "Lệnh",
"args": "Đối số JSON",
"headers": "Header JSON",
+ "oauthAfterSave": "Lưu máy chủ, sau đó chọn Kết nối để đăng nhập.",
+ "headersHelp": "Thêm các header yêu cầu mà máy chủ này sử dụng.",
"env": "Môi trường JSON",
"timeout": "Thời gian chờ công cụ",
"advancedOptions": "Tùy chọn nâng cao",
@@ -542,6 +547,21 @@
"keepExisting": "Để trống để giữ giá trị hiện tại",
"statusConfigured": "Đã cấu hình",
"statusMissingCredentials": "Cần khóa",
+ "connectingAccount": "Đang kết nối {{name}}",
+ "connectingLabel": "Đang kết nối…",
+ "continueSignIn": "Tiếp tục đăng nhập",
+ "preparingSignIn": "Đang chuẩn bị đăng nhập an toàn…",
+ "openSignInToContinue": "Mở trang đăng nhập để tiếp tục.",
+ "finishSignInInBrowser": "Hoàn tất đăng nhập trong cửa sổ trình duyệt.",
+ "manualCallbackRequired": "Hoàn tất đăng nhập, rồi dán URL callback vào nanobot.",
+ "manualCallbackHelp": "Sau khi phê duyệt quyền truy cập, trang localhost sẽ không tải được. Hãy sao chép URL đầy đủ từ thanh địa chỉ và dán vào đây.",
+ "finishingConnection": "Đang hoàn tất kết nối…",
+ "activatingTools": "Đang kích hoạt công cụ…",
+ "connected": "Đã kết nối.",
+ "connectionFailed": "Kết nối thất bại.",
+ "connectionCancelled": "Đã hủy kết nối.",
+ "reloadFailed": "Đã đăng nhập nhưng nanobot không thể kết nối các công cụ. Hãy thử khởi động lại nanobot.",
+ "oauthFailed": "Không thể kết nối. Hãy thử đăng nhập lại.",
"statusMissingDependency": "Cần phụ thuộc",
"statusComingSoon": "Sắp ra mắt",
"comingSoon": "Sắp ra mắt",
@@ -570,27 +590,28 @@
"apps": {
"description": "Thêm công cụ vào nanobot, sau đó dùng @ trong cuộc trò chuyện.",
"cliLabel": "Ứng dụng",
- "mcpLabel": "Tích hợp",
+ "mcpLabel": "MCP",
"channelLabel": "Kênh",
"featureLabel": "Tính năng",
"filterAll": "Sẵn sàng",
"filterPlugins": "Plugin",
"filterCli": "Ứng dụng",
- "filterMcp": "Tích hợp",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} sẵn sàng",
- "caption": "{{cli}} ứng dụng · {{mcp}} tích hợp",
+ "caption": "{{cli}} ứng dụng · {{mcp}} công cụ MCP",
"searchPlaceholder": "Tìm ứng dụng",
"featured": "Công cụ",
+ "mcpTools": "Công cụ MCP",
"loading": "Đang tải ứng dụng...",
"empty": "Không có công cụ phù hợp với tìm kiếm của bạn.",
"emptyApps": "Không có ứng dụng nào.",
- "emptyIntegrations": "Không có tích hợp nào.",
+ "emptyIntegrations": "Không có công cụ MCP nào.",
"emptyReady": "Chưa có công cụ nào sẵn sàng.",
"clearSearch": "Xóa tìm kiếm",
"browseApps": "Xem ứng dụng",
- "browseIntegrations": "Xem tích hợp",
- "emptyIntegrationsHint": "Thêm tích hợp tùy chỉnh ở bên dưới.",
- "restartRequired": "Khởi động lại nanobot để áp dụng ứng dụng và tính năng đã cập nhật."
+ "browseIntegrations": "Xem công cụ MCP",
+ "emptyIntegrationsHint": "Thêm máy chủ MCP tùy chỉnh ở bên dưới.",
+ "restartRequired": "Khởi động lại nanobot để áp dụng ứng dụng và công cụ MCP đã cập nhật."
},
"channels": {
"description": "Kết nối nanobot với các ứng dụng chat. Cài đặt hỗ trợ chỉ thêm gói tích hợp; hầu hết kênh vẫn cần token hoặc cấu hình không gian làm việc.",
diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json
index 265f81797..e21a8d92e 100644
--- a/webui/src/i18n/locales/zh-CN/common.json
+++ b/webui/src/i18n/locales/zh-CN/common.json
@@ -319,8 +319,8 @@
"filterInstalled": "已启用",
"filterNotInstalled": "未启用",
"searchPlaceholder": "搜索 MCP 预设",
- "moreOptions": "添加集成",
- "moreOptionsSubtitle": "连接自定义工具服务,或导入已有配置。",
+ "moreOptions": "添加 MCP 服务",
+ "moreOptionsSubtitle": "连接自定义 MCP 服务,或导入已有配置。",
"customTitle": "自定义 MCP",
"customSubtitle": "添加任意 stdio、HTTP 或 SSE MCP 服务。",
"customAction": "自定义",
@@ -328,9 +328,14 @@
"serverName": "服务名",
"serverUrl": "URL",
"transport": "传输方式",
+ "authentication": "身份验证",
+ "authNone": "无",
+ "authHeaders": "请求头",
"command": "命令",
"args": "Args JSON",
- "headers": "Headers JSON",
+ "headers": "请求头 JSON",
+ "oauthAfterSave": "保存服务器后,选择“连接”以完成登录。",
+ "headersHelp": "添加此服务器要求的请求头。",
"env": "Env JSON",
"timeout": "工具超时",
"advancedOptions": "高级选项",
@@ -357,6 +362,21 @@
"keepExisting": "留空则保留当前值",
"statusConfigured": "已配置",
"statusMissingCredentials": "需要密钥",
+ "connectingAccount": "正在连接 {{name}}",
+ "connectingLabel": "正在连接…",
+ "continueSignIn": "继续登录",
+ "preparingSignIn": "正在准备安全登录…",
+ "openSignInToContinue": "打开登录页面以继续。",
+ "finishSignInInBrowser": "请在浏览器窗口中完成登录。",
+ "manualCallbackRequired": "完成登录后,将回调 URL 粘贴到 nanobot。",
+ "manualCallbackHelp": "授权后,localhost 页面将无法打开。请复制地址栏中的完整 URL 并粘贴到这里。",
+ "finishingConnection": "正在完成连接…",
+ "activatingTools": "正在启用工具…",
+ "connected": "已连接。",
+ "connectionFailed": "连接失败。",
+ "connectionCancelled": "已取消连接。",
+ "reloadFailed": "已登录,但 nanobot 无法连接这些工具。请尝试重启 nanobot。",
+ "oauthFailed": "无法连接,请重新登录。",
"statusMissingDependency": "缺少依赖",
"statusComingSoon": "暂不支持",
"comingSoon": "即将推出",
@@ -584,27 +604,28 @@
"apps": {
"description": "将工具接入 nanobot,然后在对话中通过 @ 调用。",
"cliLabel": "应用",
- "mcpLabel": "集成",
+ "mcpLabel": "MCP",
"channelLabel": "渠道",
"featureLabel": "能力",
"filterAll": "可用",
"filterPlugins": "插件",
"filterCli": "应用",
- "filterMcp": "集成",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} 个可用",
- "caption": "{{cli}} 个应用 · {{mcp}} 个集成",
+ "caption": "{{cli}} 个应用 · {{mcp}} 个 MCP 工具",
"searchPlaceholder": "搜索工具",
"featured": "工具",
+ "mcpTools": "MCP 工具",
"loading": "正在加载应用...",
"empty": "没有与搜索条件匹配的工具。",
"emptyApps": "暂无可用应用。",
- "emptyIntegrations": "暂无可用集成。",
+ "emptyIntegrations": "暂无可用 MCP 工具。",
"emptyReady": "还没有就绪的工具。",
"clearSearch": "清除搜索",
"browseApps": "浏览应用",
- "browseIntegrations": "浏览集成",
- "emptyIntegrationsHint": "可在下方添加自定义集成。",
- "restartRequired": "重启 nanobot 以应用更新后的应用和集成。"
+ "browseIntegrations": "浏览 MCP 工具",
+ "emptyIntegrationsHint": "可在下方添加自定义 MCP 服务器。",
+ "restartRequired": "重启 nanobot 以应用更新后的应用和 MCP 工具。"
},
"channels": {
"description": "把聊天应用、邮箱和 WebUI 连接到 nanobot。",
diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json
index a2eef59f5..1983bc23e 100644
--- a/webui/src/i18n/locales/zh-TW/common.json
+++ b/webui/src/i18n/locales/zh-TW/common.json
@@ -504,8 +504,8 @@
"filterInstalled": "已啟用",
"filterNotInstalled": "未啟用",
"searchPlaceholder": "搜尋 MCP 預設",
- "moreOptions": "新增整合",
- "moreOptionsSubtitle": "連線自訂工具伺服器,或匯入現有設定。",
+ "moreOptions": "新增 MCP 服務",
+ "moreOptionsSubtitle": "連線自訂 MCP 服務,或匯入現有設定。",
"customTitle": "自訂 MCP",
"customSubtitle": "新增任何 stdio、HTTP 或 SSE MCP 伺服器。",
"customAction": "自訂",
@@ -513,9 +513,14 @@
"serverName": "伺服器名稱",
"serverUrl": "URL",
"transport": "傳輸方式",
+ "authentication": "驗證方式",
+ "authNone": "無",
+ "authHeaders": "請求標頭",
"command": "指令",
"args": "Args JSON",
- "headers": "Headers JSON",
+ "headers": "請求標頭 JSON",
+ "oauthAfterSave": "儲存伺服器後,選擇「連線」以登入。",
+ "headersHelp": "新增此伺服器使用的請求標頭。",
"env": "Env JSON",
"timeout": "工具逾時",
"advancedOptions": "進階選項",
@@ -542,6 +547,21 @@
"keepExisting": "留空以保留目前值",
"statusConfigured": "已設定",
"statusMissingCredentials": "需要金鑰",
+ "connectingAccount": "正在連接 {{name}}",
+ "connectingLabel": "正在連線…",
+ "continueSignIn": "繼續登入",
+ "preparingSignIn": "正在準備安全登入…",
+ "openSignInToContinue": "開啟登入頁面以繼續。",
+ "finishSignInInBrowser": "請在瀏覽器視窗中完成登入。",
+ "manualCallbackRequired": "完成登入後,將回呼 URL 貼到 nanobot。",
+ "manualCallbackHelp": "授權後,localhost 頁面將無法開啟。請複製網址列中的完整 URL 並貼到這裡。",
+ "finishingConnection": "正在完成連線…",
+ "activatingTools": "正在啟用工具…",
+ "connected": "已連線。",
+ "connectionFailed": "連線失敗。",
+ "connectionCancelled": "已取消連線。",
+ "reloadFailed": "已登入,但 nanobot 無法連接這些工具。請嘗試重新啟動 nanobot。",
+ "oauthFailed": "無法連線,請重新登入。",
"statusMissingDependency": "需要相依項",
"statusComingSoon": "即將推出",
"comingSoon": "即將推出",
@@ -570,27 +590,28 @@
"apps": {
"description": "將工具新增至 nanobot,再於聊天中使用 @ 指定工具。",
"cliLabel": "應用程式",
- "mcpLabel": "整合",
+ "mcpLabel": "MCP",
"channelLabel": "通訊管道",
"featureLabel": "功能",
"filterAll": "就緒",
"filterPlugins": "外掛程式",
"filterCli": "應用程式",
- "filterMcp": "整合",
+ "filterMcp": "MCP",
"enabledSummary": "{{count}} 個就緒",
- "caption": "{{cli}} 個應用程式 · {{mcp}} 個整合服務",
+ "caption": "{{cli}} 個應用程式 · {{mcp}} 個 MCP 工具",
"searchPlaceholder": "搜尋工具",
"featured": "工具",
+ "mcpTools": "MCP 工具",
"loading": "正在載入應用程式…",
"empty": "沒有符合搜尋條件的工具。",
"emptyApps": "沒有可用的應用程式。",
- "emptyIntegrations": "沒有可用的整合服務。",
+ "emptyIntegrations": "沒有可用的 MCP 工具。",
"emptyReady": "尚無就緒的工具。",
"clearSearch": "清除搜尋",
"browseApps": "瀏覽應用程式",
- "browseIntegrations": "瀏覽整合服務",
- "emptyIntegrationsHint": "可在下方新增自訂整合服務。",
- "restartRequired": "重新啟動 nanobot 以套用更新後的應用程式與整合服務。"
+ "browseIntegrations": "瀏覽 MCP 工具",
+ "emptyIntegrationsHint": "可在下方新增自訂 MCP 伺服器。",
+ "restartRequired": "重新啟動 nanobot 以套用更新後的應用程式與 MCP 工具。"
},
"channels": {
"description": "將聊天應用程式、電子郵件與 WebUI 連線至 nanobot。",
diff --git a/webui/src/lib/api.ts b/webui/src/lib/api.ts
index 8cae4885a..976b109fb 100644
--- a/webui/src/lib/api.ts
+++ b/webui/src/lib/api.ts
@@ -10,6 +10,7 @@ import type {
FilePreviewPayload,
ImageGenerationSettingsUpdate,
McpPresetsPayload,
+ McpOAuthFlowPayload,
MarketplaceProvider,
NanobotFeaturesPayload,
ModelConfigurationCreate,
@@ -97,7 +98,19 @@ async function request(
);
if (!res.ok) {
const text = typeof res.text === "function" ? (await res.text()).trim() : "";
- throw new ApiError(res.status, text || `HTTP ${res.status}`);
+ let message = text;
+ if (text.startsWith("{")) {
+ try {
+ const payload: unknown = JSON.parse(text);
+ if (payload && typeof payload === "object") {
+ const error = (payload as { error?: unknown }).error;
+ if (typeof error === "string" && error.trim()) message = error.trim();
+ }
+ } catch {
+ // Preserve non-JSON error bodies exactly as returned by the gateway.
+ }
+ }
+ throw new ApiError(res.status, message || `HTTP ${res.status}`);
}
const contentType = res.headers?.get?.("content-type") ?? "";
if (contentType && !contentType.toLowerCase().includes("application/json")) {
@@ -686,6 +699,56 @@ export async function fetchMcpPresets(
);
}
+export async function startMcpOAuth(
+ transport: WebUIMutationTransport,
+ name: string,
+ reset: boolean = false,
+): Promise {
+ return mutation(
+ transport,
+ "settings.mcp.oauth_start",
+ { name, ...(reset ? { reset: true } : {}) },
+ 30_000,
+ );
+}
+
+export async function fetchMcpOAuthStatus(
+ token: string,
+ flowId: string,
+ base: string = "",
+): Promise {
+ const query = new URLSearchParams({ flow_id: flowId });
+ return request(
+ `${base}/api/settings/mcp-oauth/status?${query}`,
+ token,
+ undefined,
+ API_READ_TIMEOUT_MS,
+ );
+}
+
+export async function completeMcpOAuth(
+ transport: WebUIMutationTransport,
+ flowId: string,
+ callbackUrl: string,
+): Promise {
+ return mutation(
+ transport,
+ "settings.mcp.oauth_complete",
+ { flow_id: flowId, callback_url: callbackUrl },
+ );
+}
+
+export async function cancelMcpOAuth(
+ transport: WebUIMutationTransport,
+ flowId: string,
+): Promise {
+ return mutation(
+ transport,
+ "settings.mcp.oauth_cancel",
+ { flow_id: flowId },
+ );
+}
+
export async function fetchProviderModels(
token: string,
provider: string,
diff --git a/webui/src/lib/types.ts b/webui/src/lib/types.ts
index 714e6f38b..2f58de2a2 100644
--- a/webui/src/lib/types.ts
+++ b/webui/src/lib/types.ts
@@ -956,6 +956,7 @@ export interface McpPresetInfo {
description: string;
docs_url: string;
transport: "stdio" | "streamableHttp" | "sse" | "oauth" | string;
+ auth?: "oauth" | null;
requires: string;
note: string;
install_supported: boolean;
@@ -977,6 +978,30 @@ export interface McpPresetInfo {
manifest?: AppManifest;
}
+export type McpOAuthFlowStatus =
+ | "starting"
+ | "authorization_required"
+ | "connecting"
+ | "authorized"
+ | "connected"
+ | "failed"
+ | "cancelled";
+
+export interface McpOAuthFlowPayload {
+ flow_id: string;
+ name: string;
+ status: McpOAuthFlowStatus;
+ expires_in: number;
+ authorization_url?: string;
+ completion_input?: "callback_url";
+ error?: string;
+ hot_reload?: {
+ ok: boolean;
+ message?: string;
+ requires_restart?: boolean;
+ };
+}
+
export interface McpPresetsPayload {
presets: McpPresetInfo[];
installed_count: number;
diff --git a/webui/src/tests/api.test.ts b/webui/src/tests/api.test.ts
index 6f80325ad..e1abd5c78 100644
--- a/webui/src/tests/api.test.ts
+++ b/webui/src/tests/api.test.ts
@@ -1,7 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
+ cancelMcpOAuth,
configureChannel,
+ completeMcpOAuth,
completeProviderOAuth,
createModelConfiguration,
createProviderSettings,
@@ -14,6 +16,7 @@ import {
fetchApiService,
fetchCliApps,
fetchInstalledCliApps,
+ fetchMcpOAuthStatus,
fetchMcpPresets,
fetchMarketplaceSkillTrends,
fetchNanobotFeatures,
@@ -41,6 +44,7 @@ import {
saveCustomMcpServer,
searchMarketplaceSkills,
startApiService,
+ startMcpOAuth,
stopApiService,
cancelChannelConnect,
pollChannelConnect,
@@ -545,6 +549,23 @@ describe("webui API helpers", () => {
});
});
+ it("extracts user-facing messages from JSON API errors", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue({
+ ok: false,
+ status: 400,
+ headers: new Headers({ "content-type": "application/json" }),
+ text: async () => JSON.stringify({ error: "Paste the complete callback URL." }),
+ }),
+ );
+
+ await expect(fetchMcpOAuthStatus("tok", "flow-123")).rejects.toMatchObject({
+ status: 400,
+ message: "Paste the complete callback URL.",
+ });
+ });
+
it("times out when an API request never responds", async () => {
vi.useFakeTimers();
vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {})));
@@ -889,6 +910,35 @@ describe("webui API helpers", () => {
{ name: "plugin-desktop" },
20_000,
);
+
+ await startMcpOAuth(mutationTransport, "notion", true);
+ expect(requestMutation).toHaveBeenCalledWith(
+ "settings.mcp.oauth_start",
+ { name: "notion", reset: true },
+ 30_000,
+ );
+
+ await fetchMcpOAuthStatus("tok", "flow-123");
+ expect(fetch).toHaveBeenCalledWith(
+ "/api/settings/mcp-oauth/status?flow_id=flow-123",
+ expect.objectContaining({ headers: { Authorization: "Bearer tok" } }),
+ );
+
+ const callbackUrl =
+ "http://127.0.0.1:8765/auth/mcp/callback?code=secret&state=state-123";
+ await completeMcpOAuth(mutationTransport, "flow-123", callbackUrl);
+ expect(requestMutation).toHaveBeenCalledWith(
+ "settings.mcp.oauth_complete",
+ { flow_id: "flow-123", callback_url: callbackUrl },
+ 20_000,
+ );
+
+ await cancelMcpOAuth(mutationTransport, "flow-123");
+ expect(requestMutation).toHaveBeenCalledWith(
+ "settings.mcp.oauth_cancel",
+ { flow_id: "flow-123" },
+ 20_000,
+ );
});
it("serializes custom MCP, mcp.json import, and tool allowlist actions", async () => {
@@ -906,6 +956,19 @@ describe("webui API helpers", () => {
20_000,
);
+ const oauthCustom = {
+ name: "company-mcp",
+ transport: "streamableHttp",
+ url: "https://mcp.example.com/mcp",
+ auth: "oauth",
+ };
+ await saveCustomMcpServer(mutationTransport, oauthCustom);
+ expect(requestMutation).toHaveBeenLastCalledWith(
+ "settings.mcp.custom",
+ oauthCustom,
+ 20_000,
+ );
+
await importMcpConfig(
mutationTransport,
'{"mcpServers":{"docs":{"command":"npx"}}}',
diff --git a/webui/src/tests/i18n.test.tsx b/webui/src/tests/i18n.test.tsx
index 2c0bcba47..b3b5a68a5 100644
--- a/webui/src/tests/i18n.test.tsx
+++ b/webui/src/tests/i18n.test.tsx
@@ -75,6 +75,18 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
"settings.apps.description",
"settings.apps.caption",
"settings.apps.restartRequired",
+ "settings.mcp.connectingAccount",
+ "settings.mcp.continueSignIn",
+ "settings.mcp.preparingSignIn",
+ "settings.mcp.openSignInToContinue",
+ "settings.mcp.finishSignInInBrowser",
+ "settings.mcp.finishingConnection",
+ "settings.mcp.activatingTools",
+ "settings.mcp.connected",
+ "settings.mcp.connectionFailed",
+ "settings.mcp.connectionCancelled",
+ "settings.mcp.reloadFailed",
+ "settings.mcp.oauthFailed",
"settings.skills.views",
"settings.skills.installedTab",
"settings.skills.discoverTab",
diff --git a/webui/src/tests/settings-view.test.tsx b/webui/src/tests/settings-view.test.tsx
index 00e441779..9acc9723e 100644
--- a/webui/src/tests/settings-view.test.tsx
+++ b/webui/src/tests/settings-view.test.tsx
@@ -335,6 +335,29 @@ const installedAnyGen = {
skill_installed: true,
};
+const xmindMcpPreset = {
+ name: "xmind",
+ display_name: "Xmind",
+ category: "productivity",
+ description: "Create, read, and edit cloud mind maps through Xmind.",
+ docs_url: "https://xmind.com/user-guide/xmind-mcp",
+ transport: "streamableHttp",
+ auth: "oauth" as const,
+ requires: "Xmind account",
+ note: "Connects securely in your browser with Xmind OAuth.",
+ install_supported: true,
+ installed: false,
+ configured: false,
+ available: false,
+ status: "not_installed",
+ logo_url: null,
+ brand_color: "#F4B41A",
+ required_fields: [],
+ connection_summary: "",
+ enabled_tools: ["*"],
+ source: "preset",
+};
+
function renderSettingsView(
options: {
initialSection?:
@@ -716,8 +739,7 @@ describe("SettingsView Apps catalog", () => {
20_000,
);
});
- expect(await screen.findByText("Computer Use enabled.")).toBeInTheDocument();
- const enabledButton = screen.getByRole("button", { name: "Enabled" });
+ const enabledButton = await screen.findByRole("button", { name: "Computer Use: Enabled" });
await waitFor(() => expect(enabledButton).toBeEnabled());
fireEvent.pointerDown(enabledButton, { button: 0, ctrlKey: false });
fireEvent.click(await screen.findByRole("menuitem", { name: "Disable" }));
@@ -729,8 +751,535 @@ describe("SettingsView Apps catalog", () => {
20_000,
);
});
- expect(await screen.findByText("Computer Use disabled.")).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Enable" })).toBeInTheDocument();
+ expect(await screen.findByRole("button", { name: "Enable" })).toBeInTheDocument();
+ });
+
+ it("connects an OAuth MCP from the Apps catalog without manual callback input", async () => {
+ let connected = false;
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") {
+ return jsonResponse({ apps: [], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({
+ presets: [connected
+ ? {
+ ...xmindMcpPreset,
+ installed: true,
+ configured: true,
+ available: true,
+ status: "configured",
+ connection_summary: "https://app.xmind.com/api/mcp",
+ }
+ : xmindMcpPreset],
+ installed_count: connected ? 1 : 0,
+ });
+ }
+ if (url === "/api/settings/mcp-oauth/status?flow_id=flow-123") {
+ connected = true;
+ return jsonResponse({
+ flow_id: "flow-123",
+ name: "xmind",
+ status: "connected",
+ expires_in: 295,
+ hot_reload: {
+ ok: false,
+ requires_restart: false,
+ connected: ["xmind"],
+ failed: ["notion"],
+ message: "MCP config reloaded, but some servers did not connect: notion",
+ },
+ });
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.oauth_start") {
+ return {
+ flow_id: "flow-123",
+ name: "xmind",
+ status: "authorization_required",
+ expires_in: 300,
+ authorization_url: "https://accounts.xmind.test/authorize?state=state-123",
+ };
+ }
+ return settingsPayload();
+ });
+ const replace = vi.fn();
+ const popup = {
+ opener: window,
+ closed: false,
+ location: { replace },
+ document: { title: "", body: { textContent: "" } },
+ focus: vi.fn(),
+ close: vi.fn(),
+ };
+ const open = vi.fn(() => popup);
+ vi.stubGlobal("open", open);
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ expect(screen.getByText("MCP tools")).toBeInTheDocument();
+ const connectButton = await screen.findByRole("button", { name: "Connect Xmind" });
+ expect(connectButton).toHaveTextContent("Connect");
+ fireEvent.click(connectButton);
+
+ expect(open).toHaveBeenCalledWith(
+ "about:blank",
+ "nanobot-mcp-oauth",
+ "popup,width=560,height=720,resizable=yes,scrollbars=yes",
+ );
+ await waitFor(() => expect(replace).toHaveBeenCalledWith(
+ "https://accounts.xmind.test/authorize?state=state-123",
+ ));
+ expect(popup.opener).toBeNull();
+ expect(screen.queryByRole("textbox", { name: /authorization/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ expect(screen.getByRole("status")).toHaveTextContent(
+ "Finish signing in in the browser window.",
+ );
+ expect(screen.getByRole("button", { name: "Connecting Xmind" })).toHaveTextContent(
+ "Connecting…",
+ );
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
+
+ expect(await screen.findByRole("button", { name: "Xmind: Configured" }, { timeout: 2500 }))
+ .toHaveTextContent("Configured");
+ expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
+ expect(screen.queryByText("Xmind connected.")).not.toBeInTheDocument();
+ expect(screen.queryByText(/some servers did not connect: notion/i)).not.toBeInTheDocument();
+ expect(popup.close).toHaveBeenCalledTimes(1);
+ expect(replace).toHaveBeenCalledTimes(1);
+ expect(fetchMock).toHaveBeenCalledWith(
+ "/api/settings/mcp-oauth/status?flow_id=flow-123",
+ expect.objectContaining({ headers: { Authorization: "Bearer tok" } }),
+ );
+ });
+
+ it("configures OAuth for a custom remote MCP without importing JSON", async () => {
+ const customPreset = {
+ ...xmindMcpPreset,
+ name: "team-mcp",
+ display_name: "team-mcp",
+ source: "custom",
+ installed: true,
+ status: "authorization_required",
+ connection_summary: "https://mcp.example.com/mcp",
+ };
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") {
+ return jsonResponse({ apps: [], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({ presets: [], installed_count: 0 });
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.custom") {
+ return {
+ presets: [customPreset],
+ installed_count: 1,
+ hot_reload: {
+ ok: false,
+ message: "MCP config reloaded, but some servers did not connect: team-mcp",
+ failed: ["team-mcp"],
+ },
+ last_action: { ok: true, message: "Saved custom MCP server team-mcp." },
+ };
+ }
+ return settingsPayload();
+ });
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Custom" }));
+
+ expect(screen.queryByText("Authentication")).not.toBeInTheDocument();
+ fireEvent.change(screen.getByLabelText("Server name"), {
+ target: { value: "team-mcp" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "HTTP" }));
+ fireEvent.change(screen.getByLabelText("URL"), {
+ target: { value: "https://mcp.example.com/mcp" },
+ });
+
+ const authentication = screen.getByRole("group", { name: "Authentication" });
+ const oauth = within(authentication).getByRole("button", { name: "OAuth" });
+ expect(oauth).toHaveAttribute("aria-pressed", "false");
+
+ fireEvent.click(within(authentication).getByRole("button", { name: "Headers" }));
+ fireEvent.change(screen.getByLabelText("Headers JSON"), {
+ target: { value: '{"Authorization":"Bearer stale"}' },
+ });
+ expect(screen.getByText("Add the request headers used by this server.")).toBeInTheDocument();
+
+ fireEvent.click(oauth);
+ expect(oauth).toHaveAttribute("aria-pressed", "true");
+ expect(screen.queryByLabelText("Headers JSON")).not.toBeInTheDocument();
+ expect(
+ screen.getByText("Save the server, then select Connect to sign in."),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "Save MCP" }));
+
+ await waitFor(() => {
+ const saveCall = requestMutationMock.mock.calls.find(
+ ([action]) => action === "settings.mcp.custom",
+ );
+ expect(saveCall).toBeDefined();
+ const values = saveCall?.[1] as Record;
+ expect(values).toMatchObject({
+ name: "team-mcp",
+ transport: "streamableHttp",
+ url: "https://mcp.example.com/mcp",
+ auth: "oauth",
+ });
+ expect(values).not.toHaveProperty("headers");
+ expect(saveCall?.[2]).toBe(20_000);
+ });
+ expect(await screen.findByRole("button", { name: "Connect team-mcp" }))
+ .toBeInTheDocument();
+ expect(
+ screen.queryByText("MCP config reloaded, but some servers did not connect: team-mcp"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("offers a pasted callback flow when the remote WebUI uses HTTP", async () => {
+ let completed = false;
+ const callbackUrl =
+ "http://127.0.0.1:8765/auth/mcp/callback?code=oauth-code&state=manual-state";
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") {
+ return jsonResponse({ apps: [], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({
+ presets: [completed
+ ? {
+ ...xmindMcpPreset,
+ installed: true,
+ configured: true,
+ available: true,
+ status: "configured",
+ connection_summary: "https://app.xmind.com/api/mcp",
+ }
+ : xmindMcpPreset],
+ installed_count: completed ? 1 : 0,
+ });
+ }
+ if (url === "/api/settings/mcp-oauth/status?flow_id=flow-manual") {
+ return jsonResponse({
+ flow_id: "flow-manual",
+ name: "xmind",
+ status: completed ? "connected" : "authorization_required",
+ expires_in: 298,
+ completion_input: "callback_url",
+ authorization_url: completed
+ ? undefined
+ : "https://accounts.xmind.test/authorize?state=manual-state",
+ hot_reload: completed ? { ok: true, requires_restart: false } : undefined,
+ });
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.oauth_start") {
+ return {
+ flow_id: "flow-manual",
+ name: "xmind",
+ status: "authorization_required",
+ expires_in: 300,
+ completion_input: "callback_url",
+ authorization_url: "https://accounts.xmind.test/authorize?state=manual-state",
+ };
+ }
+ if (action === "settings.mcp.oauth_complete") {
+ completed = true;
+ return {
+ flow_id: "flow-manual",
+ name: "xmind",
+ status: "connecting",
+ expires_in: 299,
+ completion_input: "callback_url",
+ };
+ }
+ return settingsPayload();
+ });
+ const popup = {
+ opener: window,
+ closed: false,
+ location: { replace: vi.fn() },
+ document: { title: "", body: { textContent: "" } },
+ focus: vi.fn(),
+ close: vi.fn(),
+ };
+ vi.stubGlobal("open", vi.fn(() => popup));
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Connect Xmind" }));
+
+ const callbackInput = await screen.findByRole("textbox", { name: "Full callback URL" });
+ expect(screen.getByText(/localhost page will not load/i)).toBeInTheDocument();
+ expect(screen.getByRole("status")).toHaveTextContent(
+ "Finish signing in, then paste the callback URL into nanobot.",
+ );
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+
+ fireEvent.change(callbackInput, { target: { value: callbackUrl } });
+ fireEvent.click(screen.getByRole("button", { name: "Finish sign-in" }));
+
+ await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith(
+ "settings.mcp.oauth_complete",
+ { flow_id: "flow-manual", callback_url: callbackUrl },
+ 20_000,
+ ));
+ expect(await screen.findByRole("button", { name: "Xmind: Configured" }, { timeout: 2500 }))
+ .toHaveTextContent("Configured");
+ expect(screen.queryByRole("textbox", { name: "Full callback URL" })).not.toBeInTheDocument();
+ expect(popup.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("lets the user cancel an active OAuth connection after closing the popup", async () => {
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({ presets: [xmindMcpPreset], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-oauth/status?flow_id=flow-cancel") {
+ return new Promise(() => {});
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.oauth_start") {
+ return {
+ flow_id: "flow-cancel",
+ name: "xmind",
+ status: "authorization_required",
+ expires_in: 300,
+ authorization_url: "https://accounts.xmind.test/authorize?state=cancel",
+ };
+ }
+ if (action === "settings.mcp.oauth_cancel") {
+ return {
+ flow_id: "flow-cancel",
+ name: "xmind",
+ status: "cancelled",
+ expires_in: 299,
+ };
+ }
+ return settingsPayload();
+ });
+ const popup = {
+ opener: window,
+ closed: false,
+ location: { replace: vi.fn() },
+ document: { title: "", body: { textContent: "" } },
+ focus: vi.fn(),
+ close: vi.fn(),
+ };
+ vi.stubGlobal("open", vi.fn(() => popup));
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Connect Xmind" }));
+
+ const cancelButton = await screen.findByRole("button", { name: "Cancel" });
+ expect(screen.getByRole("button", { name: "Connecting Xmind" })).toBeInTheDocument();
+ popup.closed = true;
+ fireEvent.click(cancelButton);
+
+ await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith(
+ "settings.mcp.oauth_cancel",
+ { flow_id: "flow-cancel" },
+ 20_000,
+ ));
+ expect(await screen.findByRole("button", { name: "Connect Xmind" })).toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Connecting Xmind" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
+ expect(popup.close).not.toHaveBeenCalled();
+ });
+
+ it("silently removes an MCP when the card already shows the result", async () => {
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") {
+ return jsonResponse({ apps: [], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({
+ presets: [{
+ ...xmindMcpPreset,
+ installed: true,
+ configured: true,
+ available: true,
+ status: "configured",
+ connection_summary: "https://app.xmind.com/api/mcp",
+ }],
+ installed_count: 1,
+ });
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockResolvedValueOnce({
+ presets: [xmindMcpPreset],
+ installed_count: 0,
+ requires_restart: false,
+ hot_reload: {
+ ok: true,
+ message: "MCP config reloaded without restarting nanobot.",
+ },
+ last_action: {
+ ok: true,
+ message: "Removed MCP preset for Xmind. MCP config reloaded without restarting nanobot.",
+ removed: true,
+ },
+ });
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Remove" }));
+
+ await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith(
+ "settings.mcp.remove",
+ { name: "xmind" },
+ 20_000,
+ ));
+ expect(await screen.findByRole("button", { name: "Connect Xmind" })).toBeInTheDocument();
+ expect(screen.queryByText(/Removed MCP preset|reloaded without restarting/)).not.toBeInTheDocument();
+ });
+
+ it("offers a one-click recovery when the OAuth popup is blocked", async () => {
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({ presets: [xmindMcpPreset], installed_count: 0 });
+ }
+ return new Promise(() => {});
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.oauth_start") {
+ return {
+ flow_id: "flow-blocked",
+ name: "xmind",
+ status: "authorization_required",
+ expires_in: 300,
+ authorization_url: "https://accounts.xmind.test/authorize?state=blocked",
+ };
+ }
+ return settingsPayload();
+ });
+ const popup = {
+ opener: window,
+ closed: false,
+ location: { replace: vi.fn() },
+ focus: vi.fn(),
+ close: vi.fn(),
+ };
+ const open = vi.fn()
+ .mockReturnValueOnce(null)
+ .mockReturnValueOnce(popup);
+ vi.stubGlobal("open", open);
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Connect Xmind" }));
+
+ const continueButton = await screen.findByRole("button", { name: "Continue sign-in" });
+ expect(screen.getByRole("status")).toHaveTextContent("Open the sign-in page to continue.");
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
+ fireEvent.click(continueButton);
+ expect(open).toHaveBeenLastCalledWith(
+ "https://accounts.xmind.test/authorize?state=blocked",
+ "nanobot-mcp-oauth",
+ "popup,width=560,height=720,resizable=yes,scrollbars=yes",
+ );
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
+ });
+
+ it("does not mistake a COOP-isolated OAuth tab for a blocked popup", async () => {
+ let popupIsolated = false;
+ let statusCalls = 0;
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/settings") return jsonResponse(settingsPayload());
+ if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
+ if (url === "/api/settings/mcp-presets") {
+ return jsonResponse({ presets: [xmindMcpPreset], installed_count: 0 });
+ }
+ if (url === "/api/settings/mcp-oauth/status?flow_id=flow-coop") {
+ statusCalls += 1;
+ return jsonResponse({
+ flow_id: "flow-coop",
+ name: "xmind",
+ status: statusCalls === 1 ? "authorization_required" : "failed",
+ expires_in: 299,
+ error: statusCalls === 1 ? undefined : "Cancelled for test cleanup.",
+ authorization_url: statusCalls === 1
+ ? "https://accounts.xmind.test/authorize?state=coop"
+ : undefined,
+ });
+ }
+ return { ok: false, status: 404, text: async () => "Not found" } as Response;
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ requestMutationMock.mockImplementation(async (action: string) => {
+ if (action === "settings.mcp.oauth_start") {
+ return {
+ flow_id: "flow-coop",
+ name: "xmind",
+ status: "authorization_required",
+ expires_in: 300,
+ authorization_url: "https://accounts.xmind.test/authorize?state=coop",
+ };
+ }
+ return settingsPayload();
+ });
+ const popup = {
+ opener: window,
+ get closed() {
+ return popupIsolated;
+ },
+ location: {
+ replace: vi.fn(() => {
+ popupIsolated = true;
+ }),
+ },
+ document: { title: "", body: { textContent: "" } },
+ focus: vi.fn(),
+ close: vi.fn(),
+ };
+ vi.stubGlobal("open", vi.fn(() => popup));
+
+ renderSettingsView({ initialSection: "apps" });
+ fireEvent.click(await screen.findByRole("button", { name: "MCP" }));
+ fireEvent.click(await screen.findByRole("button", { name: "Connect Xmind" }));
+
+ await waitFor(() => expect(statusCalls).toBe(1), { timeout: 2000 });
+ expect(screen.getByRole("status")).toHaveTextContent(
+ "Finish signing in in the browser window.",
+ );
+ expect(screen.queryByRole("button", { name: "Continue sign-in" })).not.toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
});
it("keeps runtime dependencies out of Apps and explains chat mentions", async () => {
@@ -775,7 +1324,7 @@ describe("SettingsView Apps catalog", () => {
).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Ready" })).toHaveAttribute("aria-pressed", "false");
expect(screen.getByRole("button", { name: "Apps" })).toHaveAttribute("aria-pressed", "true");
- expect(screen.getByRole("button", { name: "Integrations" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "MCP" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Plugins" })).not.toBeInTheDocument();
expect(screen.queryByText("Api")).not.toBeInTheDocument();
expect(screen.queryByText("0 ready")).not.toBeInTheDocument();
@@ -2104,8 +2653,8 @@ describe("SettingsView Apps catalog", () => {
expect(await screen.findByText("No apps available.")).toBeInTheDocument();
expect(screen.queryByText("Loading Apps...")).not.toBeInTheDocument();
- fireEvent.click(screen.getByRole("button", { name: "Browse integrations" }));
- expect(await screen.findByText("Add integration")).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Browse MCP tools" }));
+ expect(await screen.findByText("Add MCP server")).toBeInTheDocument();
});
it("shows token activity on the overview", async () => {