From 5cecc556137da257ec0150b3f411990932ea25fe Mon Sep 17 00:00:00 2001 From: t8y2 Date: Fri, 16 Oct 2026 18:40:08 +0800 Subject: [PATCH] feat(mcp): detect and authorize macOS Keychain access for dbx-mcp --- .../editor/EditorSettingsDialog.vue | 66 ++++++++- apps/desktop/src/i18n/locales/az.ts | 9 ++ apps/desktop/src/i18n/locales/en.ts | 9 ++ apps/desktop/src/i18n/locales/es.ts | 9 ++ apps/desktop/src/i18n/locales/id.ts | 9 ++ apps/desktop/src/i18n/locales/it.ts | 10 ++ apps/desktop/src/i18n/locales/ja.ts | 9 ++ apps/desktop/src/i18n/locales/ko.ts | 9 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 10 ++ apps/desktop/src/i18n/locales/ru.ts | 9 ++ apps/desktop/src/i18n/locales/th.ts | 9 ++ apps/desktop/src/i18n/locales/tr.ts | 9 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 9 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 9 ++ apps/desktop/src/lib/backend/api.ts | 2 + apps/desktop/src/lib/backend/http.ts | 8 ++ apps/desktop/src/lib/backend/tauri.ts | 10 ++ docs/public/install-mcp.sh | 7 + src-tauri/src/commands/mcp.rs | 25 ++++ .../src/commands/mcp_native_installation.rs | 129 ++++++++++++++++++ src-tauri/src/lib.rs | 2 + 21 files changed, 367 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 6fd125f6f1..c85ba06fb4 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -153,6 +153,8 @@ import { loadCcSwitchAiConfigs, loadMagpieAiConfigs, checkMcpServerStatus, + checkMcpKeychainAccess, + authorizeMcpKeychain, installMcpServer, installNativeMcpServer, uninstallMcpServer, @@ -210,6 +212,7 @@ import { type WebMcpHttpStatus, type WebMcpHttpSettings, type McpServerStatus, + type McpKeychainAccess, type SnippetProvider, type SnippetSyncConfig, type SyncSelection, @@ -3892,6 +3895,11 @@ type McpManagementTab = "access" | "permissions"; const mcpStatus = ref(null); const mcpStatusLoading = ref(false); const mcpStatusError = ref(""); +const mcpKeychainAccess = ref(null); +const mcpKeychainAuthorizing = ref(false); +const mcpKeychainMessage = ref(""); +const mcpKeychainMessageError = ref(false); +const mcpKeychainNeedsAuthorization = computed(() => mcpKeychainAccess.value === "needs_authorization"); const mcpCopied = ref<"" | McpCopyKind>(""); const mcpConfigTab = ref("claude"); const mcpTransportTab = ref("stdio"); @@ -4556,7 +4564,7 @@ const mcpWorkBuddyRecommendedConfig = computed(() => buildMcpWorkBuddyConfig(mcp const mcpStatusTone = computed<"ok" | "warning" | "muted">(() => { if (!mcpStatus.value) return "muted"; - if (!mcpStatus.value.installed || mcpStatus.value.update_available || mcpStatus.value.error) return "warning"; + if (!mcpStatus.value.installed || mcpStatus.value.update_available || mcpStatus.value.error || mcpKeychainNeedsAuthorization.value) return "warning"; return "ok"; }); @@ -4566,6 +4574,7 @@ const mcpStatusLabel = computed(() => { if (!mcpStatus.value) return t("settings.mcpStatusUnknown"); if (!mcpStatus.value.installed) return t("settings.mcpNotInstalled"); if (mcpStatus.value.update_available) return t("settings.mcpUpdateAvailable"); + if (mcpKeychainNeedsAuthorization.value) return t("settings.mcpKeychainBadge"); return t("settings.mcpReady"); }); @@ -4602,6 +4611,40 @@ async function refreshMcpStatus() { } finally { mcpStatusLoading.value = false; } + await refreshMcpKeychainAccess(); +} + +// macOS only: an MCP client launches dbx-mcp in the background, where Keychain cannot prompt. +// A missing or stale authorization therefore surfaces only as an opaque tool error in the client. +async function refreshMcpKeychainAccess() { + if (!mcpStatus.value?.installed) { + mcpKeychainAccess.value = null; + return; + } + try { + mcpKeychainAccess.value = await checkMcpKeychainAccess(); + } catch { + mcpKeychainAccess.value = "unknown"; + } +} + +async function authorizeMcpKeychainAccess() { + if (mcpKeychainAuthorizing.value) return; + mcpKeychainAuthorizing.value = true; + mcpKeychainMessage.value = ""; + mcpKeychainMessageError.value = false; + try { + await authorizeMcpKeychain(); + await refreshMcpKeychainAccess(); + // The command re-verifies from a fresh process, so a stale answer here means the choice was not retained. + mcpKeychainMessageError.value = mcpKeychainNeedsAuthorization.value; + mcpKeychainMessage.value = mcpKeychainNeedsAuthorization.value ? t("settings.mcpKeychainNotRetained") : t("settings.mcpKeychainAuthorized"); + } catch (e: any) { + mcpKeychainMessage.value = t("settings.mcpKeychainAuthorizeFailed", { error: e?.message || String(e) }); + mcpKeychainMessageError.value = true; + } finally { + mcpKeychainAuthorizing.value = false; + } } async function copyMcpText(kind: McpCopyKind, value: string) { @@ -12086,6 +12129,27 @@ LIMIT 100; +
+ +

{{ mcpKeychainMessage }}

+
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 4a27afeeef..85bfe0557b 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -8326,6 +8326,15 @@ export default withEnglishFallback({ mcpStatusError: "Yoxlama uğursuz oldu", mcpReady: "Hazırdır", mcpNotInstalled: "Quraşdırılmayıb", + mcpKeychainBadge: "Açar zənciri icazəsi lazımdır", + mcpKeychainTitle: "Açar zəncirinə giriş üçün icazə tələb olunur", + mcpKeychainDescription: "MCP müştəriləri dbx-mcp-ni arxa planda işə salır, orada macOS açar zənciri sorğusunu göstərə bilmir, ona görə DBX məlumat şifrələmə açarını oxuya bilmir və bütün MCP alət çağırışları SECRET_KEY_UNAVAILABLE ilə uğursuz olur. Burada bir dəfə icazə verin.", + mcpKeychainAuthorizeButton: "Açar zəncirinə girişə icazə ver", + mcpKeychainAuthorizing: "İcazə gözlənilir...", + mcpKeychainAlwaysAllowHint: 'macOS soruşanda "Həmişə icazə ver" seçin ("İcazə ver" yox).', + mcpKeychainAuthorized: "Açar zəncirinə giriş icazəsi verildi. Qüvvəyə minməsi üçün MCP müştərisini yenidən başladın.", + mcpKeychainNotRetained: 'macOS icazəni saxlamadı. Yenidən işə salın və "Həmişə icazə ver" seçin ("İcazə ver" yox). Giriş açar zənciri kilidlidirsə, əvvəlcə kilidini açın.', + mcpKeychainAuthorizeFailed: "Açar zənciri icazəsi uğursuz oldu: {error}", mcpUpdateAvailable: "Yeniləmə mövcuddur", mcpCurrent: "Cari versiya", mcpLatest: "Ən son versiya", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fb978d9e55..f843e22324 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -9829,6 +9829,15 @@ export default { mcpStatusError: "Check failed", mcpReady: "Ready", mcpNotInstalled: "Not installed", + mcpKeychainBadge: "Keychain authorization needed", + mcpKeychainTitle: "Keychain authorization required", + mcpKeychainDescription: "MCP clients start dbx-mcp in the background, where macOS cannot show a Keychain prompt, so it cannot read the DBX data encryption key and every MCP tool call fails with SECRET_KEY_UNAVAILABLE. Authorize it once here.", + mcpKeychainAuthorizeButton: "Authorize Keychain access", + mcpKeychainAuthorizing: "Waiting for authorization...", + mcpKeychainAlwaysAllowHint: 'When macOS asks, choose "Always Allow" (not "Allow").', + mcpKeychainAuthorized: "Keychain access authorized. Restart your MCP client for it to take effect.", + mcpKeychainNotRetained: 'macOS did not retain the authorization. Run it again and choose "Always Allow" (not "Allow"). Unlock the login keychain first if it is locked.', + mcpKeychainAuthorizeFailed: "Keychain authorization failed: {error}", mcpUpdateAvailable: "Update available", mcpCurrent: "Current version", mcpLatest: "Latest version", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index dfefa90f36..483bc1cc8f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -8872,6 +8872,15 @@ export default withEnglishFallback({ mcpStatusError: "Error al comprobar", mcpReady: "Listo", mcpNotInstalled: "No instalado", + mcpKeychainBadge: "Se requiere autorización del llavero", + mcpKeychainTitle: "Se requiere autorizar el acceso al llavero", + mcpKeychainDescription: "Los clientes MCP inician dbx-mcp en segundo plano, donde macOS no puede mostrar el aviso del llavero, por lo que no puede leer la clave de cifrado de datos de DBX y todas las llamadas a herramientas MCP fallan con SECRET_KEY_UNAVAILABLE. Autorícelo una sola vez aquí.", + mcpKeychainAuthorizeButton: "Autorizar acceso al llavero", + mcpKeychainAuthorizing: "Esperando autorización...", + mcpKeychainAlwaysAllowHint: 'Cuando macOS pregunte, elija "Permitir siempre" (no "Permitir").', + mcpKeychainAuthorized: "Acceso al llavero autorizado. Reinicie su cliente MCP para que surta efecto.", + mcpKeychainNotRetained: 'macOS no conservó la autorización. Vuelva a ejecutarlo y elija "Permitir siempre" (no "Permitir"). Desbloquee antes el llavero de inicio de sesión si está bloqueado.', + mcpKeychainAuthorizeFailed: "Error al autorizar el llavero: {error}", mcpUpdateAvailable: "Actualización disponible", mcpCurrent: "Versión actual", mcpLatest: "Última versión", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 80b8db813e..992fdeeae9 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -9134,6 +9134,15 @@ export default withEnglishFallback({ mcpStatusError: "Pemeriksaan gagal", mcpReady: "Siap", mcpNotInstalled: "Belum terpasang", + mcpKeychainBadge: "Otorisasi Keychain diperlukan", + mcpKeychainTitle: "Otorisasi akses Keychain diperlukan", + mcpKeychainDescription: "Klien MCP menjalankan dbx-mcp di latar belakang, tempat macOS tidak dapat menampilkan prompt Keychain, sehingga tidak dapat membaca kunci enkripsi data DBX dan setiap pemanggilan alat MCP gagal dengan SECRET_KEY_UNAVAILABLE. Otorisasi sekali saja di sini.", + mcpKeychainAuthorizeButton: "Otorisasi akses Keychain", + mcpKeychainAuthorizing: "Menunggu otorisasi...", + mcpKeychainAlwaysAllowHint: 'Saat macOS bertanya, pilih "Selalu Izinkan" (bukan "Izinkan").', + mcpKeychainAuthorized: "Akses Keychain diotorisasi. Mulai ulang klien MCP agar berlaku.", + mcpKeychainNotRetained: 'macOS tidak mempertahankan otorisasi. Jalankan lagi dan pilih "Selalu Izinkan" (bukan "Izinkan"). Buka kunci keychain login terlebih dahulu jika terkunci.', + mcpKeychainAuthorizeFailed: "Otorisasi Keychain gagal: {error}", mcpUpdateAvailable: "Pembaruan tersedia", mcpCurrent: "Versi saat ini", mcpLatest: "Versi terbaru", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 43180d4cb2..a1b94ede01 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -8635,6 +8635,16 @@ export default withEnglishFallback({ mcpStatusError: "Verifica non riuscita", mcpReady: "Pronto", mcpNotInstalled: "Non installato", + mcpKeychainBadge: "Autorizzazione portachiavi necessaria", + mcpKeychainTitle: "È necessario autorizzare l'accesso al portachiavi", + mcpKeychainDescription: + "I client MCP avviano dbx-mcp in background, dove macOS non può mostrare la richiesta del portachiavi, quindi non riesce a leggere la chiave di cifratura dei dati di DBX e ogni chiamata agli strumenti MCP fallisce con SECRET_KEY_UNAVAILABLE. Autorizzalo una sola volta qui.", + mcpKeychainAuthorizeButton: "Autorizza accesso al portachiavi", + mcpKeychainAuthorizing: "In attesa di autorizzazione...", + mcpKeychainAlwaysAllowHint: 'Quando macOS lo chiede, scegli "Consenti sempre" (non "Consenti").', + mcpKeychainAuthorized: "Accesso al portachiavi autorizzato. Riavvia il client MCP per applicarlo.", + mcpKeychainNotRetained: 'macOS non ha conservato l\'autorizzazione. Eseguilo di nuovo e scegli "Consenti sempre" (non "Consenti"). Se il portachiavi di login è bloccato, sbloccalo prima.', + mcpKeychainAuthorizeFailed: "Autorizzazione del portachiavi non riuscita: {error}", mcpUpdateAvailable: "Aggiornamento disponibile", mcpCurrent: "Versione corrente", mcpLatest: "Ultima versione", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index d2df853004..f3b101fee0 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -8625,6 +8625,15 @@ export default withEnglishFallback({ mcpStatusError: "確認失敗", mcpReady: "準備完了", mcpNotInstalled: "未インストール", + mcpKeychainBadge: "キーチェーンの承認が必要", + mcpKeychainTitle: "キーチェーンへのアクセス承認が必要です", + mcpKeychainDescription: "MCP クライアントは dbx-mcp をバックグラウンドで起動するため、macOS がキーチェーンの確認ダイアログを表示できず、DBX のデータ暗号化キーを読み取れません。その結果、すべての MCP ツール呼び出しが SECRET_KEY_UNAVAILABLE で失敗します。ここで一度だけ承認してください。", + mcpKeychainAuthorizeButton: "キーチェーンアクセスを承認", + mcpKeychainAuthorizing: "承認を待っています...", + mcpKeychainAlwaysAllowHint: "macOS の確認ダイアログでは「常に許可」を選択してください(「許可」ではありません)。", + mcpKeychainAuthorized: "キーチェーンアクセスを承認しました。反映するには MCP クライアントを再起動してください。", + mcpKeychainNotRetained: "macOS が承認を保持しませんでした。もう一度実行し、「常に許可」を選択してください(「許可」ではありません)。ログインキーチェーンがロックされている場合は先に解除してください。", + mcpKeychainAuthorizeFailed: "キーチェーンの承認に失敗しました:{error}", mcpUpdateAvailable: "アップデートがあります", mcpCurrent: "現在のバージョン", mcpLatest: "最新バージョン", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index df2b85a2e5..ef8fa20c4a 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -8531,6 +8531,15 @@ export default withEnglishFallback({ mcpStatusError: "확인 실패", mcpReady: "준비됨", mcpNotInstalled: "설치되지 않음", + mcpKeychainBadge: "키체인 승인 필요", + mcpKeychainTitle: "키체인 접근 승인이 필요합니다", + mcpKeychainDescription: "MCP 클라이언트는 dbx-mcp를 백그라운드에서 실행하므로 macOS가 키체인 확인 창을 표시할 수 없고, DBX 데이터 암호화 키를 읽지 못해 모든 MCP 도구 호출이 SECRET_KEY_UNAVAILABLE로 실패합니다. 여기서 한 번만 승인해 주세요.", + mcpKeychainAuthorizeButton: "키체인 접근 승인", + mcpKeychainAuthorizing: "승인을 기다리는 중...", + mcpKeychainAlwaysAllowHint: 'macOS 창이 나타나면 "항상 허용"을 선택하세요("허용"이 아님).', + mcpKeychainAuthorized: "키체인 접근이 승인되었습니다. 적용하려면 MCP 클라이언트를 다시 시작하세요.", + mcpKeychainNotRetained: 'macOS가 승인을 유지하지 않았습니다. 다시 실행하고 "항상 허용"을 선택하세요("허용"이 아님). 로그인 키체인이 잠겨 있다면 먼저 잠금을 해제하세요.', + mcpKeychainAuthorizeFailed: "키체인 승인 실패: {error}", mcpUpdateAvailable: "업데이트 가능", mcpCurrent: "현재 버전", mcpLatest: "최신 버전", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index c282b83a21..e64ea3cfb9 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -8633,6 +8633,16 @@ export default withEnglishFallback({ mcpStatusError: "Falha na verificação", mcpReady: "Pronto", mcpNotInstalled: "Não instalado", + mcpKeychainBadge: "Autorização do chaveiro necessária", + mcpKeychainTitle: "É necessário autorizar o acesso ao chaveiro", + mcpKeychainDescription: + "Os clientes MCP iniciam o dbx-mcp em segundo plano, onde o macOS não consegue exibir o aviso do chaveiro, então ele não consegue ler a chave de criptografia de dados do DBX e todas as chamadas de ferramentas MCP falham com SECRET_KEY_UNAVAILABLE. Autorize uma única vez aqui.", + mcpKeychainAuthorizeButton: "Autorizar acesso ao chaveiro", + mcpKeychainAuthorizing: "Aguardando autorização...", + mcpKeychainAlwaysAllowHint: 'Quando o macOS perguntar, escolha "Sempre Permitir" (não "Permitir").', + mcpKeychainAuthorized: "Acesso ao chaveiro autorizado. Reinicie seu cliente MCP para que tenha efeito.", + mcpKeychainNotRetained: 'O macOS não manteve a autorização. Execute novamente e escolha "Sempre Permitir" (não "Permitir"). Desbloqueie antes o chaveiro de login, se estiver bloqueado.', + mcpKeychainAuthorizeFailed: "Falha ao autorizar o chaveiro: {error}", mcpUpdateAvailable: "Atualização disponível", mcpCurrent: "Versão atual", mcpLatest: "Versão mais recente", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index eda6f1bd38..3e74c656c7 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -10019,6 +10019,15 @@ export default withEnglishFallback({ mcpStatusError: "Проверка не удалась", mcpReady: "Готово", mcpNotInstalled: "Не установлено", + mcpKeychainBadge: "Требуется доступ к связке ключей", + mcpKeychainTitle: "Необходимо разрешить доступ к связке ключей", + mcpKeychainDescription: "Клиенты MCP запускают dbx-mcp в фоне, где macOS не может показать запрос связки ключей, поэтому он не может прочитать ключ шифрования данных DBX, и все вызовы инструментов MCP завершаются ошибкой SECRET_KEY_UNAVAILABLE. Разрешите доступ один раз здесь.", + mcpKeychainAuthorizeButton: "Разрешить доступ к связке ключей", + mcpKeychainAuthorizing: "Ожидание разрешения...", + mcpKeychainAlwaysAllowHint: "Когда macOS спросит, выберите «Всегда разрешать» (а не «Разрешить»).", + mcpKeychainAuthorized: "Доступ к связке ключей разрешён. Перезапустите клиент MCP, чтобы изменения вступили в силу.", + mcpKeychainNotRetained: "macOS не сохранила разрешение. Запустите снова и выберите «Всегда разрешать» (а не «Разрешить»). Если связка ключей входа заблокирована, сначала разблокируйте её.", + mcpKeychainAuthorizeFailed: "Не удалось разрешить доступ к связке ключей: {error}", mcpUpdateAvailable: "Доступно обновление", mcpCurrent: "Текущая версия", mcpLatest: "Последняя версия", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index 48b7d398fc..ce132fff1a 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -10454,6 +10454,15 @@ export default withEnglishFallback({ mcpStatusError: "ตรวจสอบไม่สำเร็จ", mcpReady: "พร้อม", mcpNotInstalled: "ไม่ได้ติดตั้ง", + mcpKeychainBadge: "ต้องอนุญาตการเข้าถึง Keychain", + mcpKeychainTitle: "ต้องอนุญาตการเข้าถึง Keychain", + mcpKeychainDescription: "ไคลเอนต์ MCP เริ่ม dbx-mcp ในเบื้องหลัง ซึ่ง macOS ไม่สามารถแสดงหน้าต่างขออนุญาต Keychain ได้ จึงอ่านคีย์เข้ารหัสข้อมูลของ DBX ไม่ได้ และการเรียกเครื่องมือ MCP ทุกครั้งจะล้มเหลวด้วย SECRET_KEY_UNAVAILABLE โปรดอนุญาตเพียงครั้งเดียวที่นี่", + mcpKeychainAuthorizeButton: "อนุญาตการเข้าถึง Keychain", + mcpKeychainAuthorizing: "กำลังรอการอนุญาต...", + mcpKeychainAlwaysAllowHint: 'เมื่อ macOS ถาม ให้เลือก "อนุญาตเสมอ" (ไม่ใช่ "อนุญาต")', + mcpKeychainAuthorized: "อนุญาตการเข้าถึง Keychain แล้ว โปรดรีสตาร์ตไคลเอนต์ MCP เพื่อให้มีผล", + mcpKeychainNotRetained: 'macOS ไม่ได้จดจำการอนุญาต โปรดรันอีกครั้งและเลือก "อนุญาตเสมอ" (ไม่ใช่ "อนุญาต") หาก Keychain การเข้าสู่ระบบถูกล็อก ให้ปลดล็อกก่อน', + mcpKeychainAuthorizeFailed: "การอนุญาต Keychain ล้มเหลว: {error}", mcpUpdateAvailable: "มีการอัปเดต", mcpCurrent: "เวอร์ชันปัจจุบัน", mcpLatest: "เวอร์ชันล่าสุด", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 9b66c0a715..b76504966a 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -8228,6 +8228,15 @@ export default withEnglishFallback({ mcpStatusError: "Denetim başarısız", mcpReady: "Hazır", mcpNotInstalled: "Kurulu değil", + mcpKeychainBadge: "Anahtar zinciri yetkisi gerekli", + mcpKeychainTitle: "Anahtar zinciri erişimi için yetki gerekiyor", + mcpKeychainDescription: "MCP istemcileri dbx-mcp'yi arka planda başlatır; macOS orada anahtar zinciri isteğini gösteremez, bu yüzden DBX veri şifreleme anahtarı okunamaz ve tüm MCP araç çağrıları SECRET_KEY_UNAVAILABLE ile başarısız olur. Burada bir kez yetkilendirin.", + mcpKeychainAuthorizeButton: "Anahtar zinciri erişimini yetkilendir", + mcpKeychainAuthorizing: "Yetki bekleniyor...", + mcpKeychainAlwaysAllowHint: 'macOS sorduğunda "Her Zaman İzin Ver" seçeneğini seçin ("İzin Ver" değil).', + mcpKeychainAuthorized: "Anahtar zinciri erişimi yetkilendirildi. Etkili olması için MCP istemcinizi yeniden başlatın.", + mcpKeychainNotRetained: 'macOS yetkiyi korumadı. Tekrar çalıştırın ve "Her Zaman İzin Ver" seçeneğini seçin ("İzin Ver" değil). Oturum açma anahtar zinciri kilitliyse önce kilidini açın.', + mcpKeychainAuthorizeFailed: "Anahtar zinciri yetkilendirmesi başarısız: {error}", mcpUpdateAvailable: "Güncelleme mevcut", mcpCurrent: "Geçerli sürüm", mcpLatest: "En son sürüm", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index c4e022935e..95e6ee43e1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -9811,6 +9811,15 @@ export default withEnglishFallback({ mcpStatusError: "检查失败", mcpReady: "可用", mcpNotInstalled: "未安装", + mcpKeychainBadge: "需要钥匙串授权", + mcpKeychainTitle: "需要授权钥匙串访问", + mcpKeychainDescription: "MCP 客户端在后台启动 dbx-mcp 时,macOS 无法弹出钥匙串授权框,导致它读不到 DBX 数据加密密钥,所有 MCP 工具调用都会报 SECRET_KEY_UNAVAILABLE。请在此处一次性授权。", + mcpKeychainAuthorizeButton: "授权钥匙串访问", + mcpKeychainAuthorizing: "等待授权...", + mcpKeychainAlwaysAllowHint: "macOS 弹窗时请选择“始终允许”(不要选“允许”)。", + mcpKeychainAuthorized: "钥匙串访问已授权。请重启 MCP 客户端使其生效。", + mcpKeychainNotRetained: "macOS 没有保留该授权。请重新授权并选择“始终允许”(不要选“允许”);若登录钥匙串已锁定,请先解锁。", + mcpKeychainAuthorizeFailed: "钥匙串授权失败:{error}", mcpUpdateAvailable: "有更新", mcpCurrent: "当前版本", mcpLatest: "最新版本", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 66cd78779a..e285043300 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -8006,6 +8006,15 @@ export default withEnglishFallback({ mcpStatusError: "檢查失敗", mcpReady: "可用", mcpNotInstalled: "未安裝", + mcpKeychainBadge: "需要鑰匙圈授權", + mcpKeychainTitle: "需要授權鑰匙圈存取", + mcpKeychainDescription: "MCP 用戶端在背景啟動 dbx-mcp 時,macOS 無法彈出鑰匙圈授權視窗,導致它讀不到 DBX 資料加密金鑰,所有 MCP 工具呼叫都會回報 SECRET_KEY_UNAVAILABLE。請在此處一次性授權。", + mcpKeychainAuthorizeButton: "授權鑰匙圈存取", + mcpKeychainAuthorizing: "等待授權...", + mcpKeychainAlwaysAllowHint: "macOS 彈出視窗時請選擇「永遠允許」(不要選「允許」)。", + mcpKeychainAuthorized: "鑰匙圈存取已授權。請重新啟動 MCP 用戶端使其生效。", + mcpKeychainNotRetained: "macOS 沒有保留該授權。請重新授權並選擇「永遠允許」(不要選「允許」);若登入鑰匙圈已鎖定,請先解鎖。", + mcpKeychainAuthorizeFailed: "鑰匙圈授權失敗:{error}", mcpUpdateAvailable: "有更新", mcpCurrent: "當前版本", mcpLatest: "最新版本", diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index eb6a0bf574..b80dcbf84c 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -978,6 +978,8 @@ export const deleteHistoryEntry = forward("deleteHistoryEntry"); // Updates export const checkMcpServerStatus = forward("checkMcpServerStatus"); +export const checkMcpKeychainAccess = forward("checkMcpKeychainAccess"); +export const authorizeMcpKeychain = forward("authorizeMcpKeychain"); export const installMcpServer = forward("installMcpServer"); export const installNativeMcpServer = forward("installNativeMcpServer"); export const uninstallMcpServer = forward("uninstallMcpServer"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 23efa74fc9..9d813dfa49 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -5627,6 +5627,14 @@ export async function checkMcpServerStatus(): Promise { + return "not_applicable"; +} + +export async function authorizeMcpKeychain(): Promise { + throw new Error("MCP Keychain authorization is only available in the desktop app."); +} + export async function installMcpServer(): Promise { throw new Error("MCP Server installation is only available in the desktop app."); } diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 59649a7fe5..9aeb5fc606 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -3416,6 +3416,16 @@ export async function checkMcpServerStatus(): Promise { return invoke("check_mcp_server_status"); } +export type McpKeychainAccess = "authorized" | "needs_authorization" | "key_missing" | "unknown" | "not_applicable"; + +export async function checkMcpKeychainAccess(): Promise { + return invoke("check_mcp_keychain_access"); +} + +export async function authorizeMcpKeychain(): Promise { + return invoke("authorize_mcp_keychain"); +} + export async function installMcpServer(): Promise { return invoke("install_mcp_server"); } diff --git a/docs/public/install-mcp.sh b/docs/public/install-mcp.sh index 00258e433a..255c040df0 100644 --- a/docs/public/install-mcp.sh +++ b/docs/public/install-mcp.sh @@ -169,3 +169,10 @@ mv -f "$work_dir/version" "$marker" configure_path printf 'Installed dbx-mcp %s at %s\n' "$version" "$binary" print_configs +if [ "$operating_system" = Darwin ]; then + printf '\nmacOS: authorize Keychain access once, or MCP clients cannot read the DBX encryption key.\n' + printf ' 1. Unlock your login keychain.\n' + printf ' 2. Run: %s --authorize-keychain\n' "$binary" + printf ' 3. When macOS asks, choose "Always Allow" (not "Allow"), then restart your MCP client.\n' + printf 'DBX Desktop can do this for you: Settings > MCP.\n' +fi diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index a71015ed5c..fd4df0b7af 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -363,6 +363,31 @@ pub async fn check_mcp_server_status(app: AppHandle) -> Result Result { + tauri::async_runtime::spawn_blocking(|| { + native_installation::resolve() + .map_or(native_installation::KeychainAccess::NotApplicable, |native| native.keychain_access()) + }) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn authorize_mcp_keychain() -> Result { + tauri::async_runtime::spawn_blocking(|| { + let native = native_installation::resolve().ok_or_else(|| { + "The native DBX MCP is not installed; install it before authorizing Keychain access.".to_string() + })?; + native.authorize_keychain() + }) + .await + .map_err(|e| e.to_string())? +} + #[tauri::command] pub async fn install_mcp_server() -> Result { tauri::async_runtime::spawn_blocking(|| { diff --git a/src-tauri/src/commands/mcp_native_installation.rs b/src-tauri/src/commands/mcp_native_installation.rs index ff2f674854..b546b60bf3 100644 --- a/src-tauri/src/commands/mcp_native_installation.rs +++ b/src-tauri/src/commands/mcp_native_installation.rs @@ -133,6 +133,66 @@ fn discover_binary( Some(NativeInstallation { binary, version, kind }) } +/// Result of probing whether a background `dbx-mcp` launch can read the DBX +/// encryption key from the OS credential store without user interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum KeychainAccess { + Authorized, + NeedsAuthorization, + /// No DBX key exists yet; Desktop's data-security setup must run first. + KeyMissing, + /// The probe could not be interpreted, e.g. a binary predating the probe flag. + Unknown, + /// Not macOS, or no native `dbx-mcp` is installed: nothing to authorize. + NotApplicable, +} + +const VERIFY_KEYCHAIN_ARGUMENT: &str = "--verify-keychain-access"; +const AUTHORIZE_KEYCHAIN_ARGUMENT: &str = "--authorize-keychain"; +const VERIFY_KEYCHAIN_TIMEOUT: Duration = Duration::from_secs(5); +// The user has to answer the macOS Keychain dialog, so allow time for it. +const AUTHORIZE_KEYCHAIN_TIMEOUT: Duration = Duration::from_secs(180); + +fn classify_keychain_probe(success: bool, stderr: &str) -> KeychainAccess { + if success { + KeychainAccess::Authorized + } else if stderr.contains("KEYRING_ACCESS_FAILED") { + KeychainAccess::NeedsAuthorization + } else if stderr.contains("SECRET_KEY_UNAVAILABLE") { + KeychainAccess::KeyMissing + } else { + KeychainAccess::Unknown + } +} + +fn run_with_timeout(binary: &Path, argument: &str, timeout: Duration) -> Result { + let mut child = dbx_core::process::new_std_command(binary) + .arg(argument) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| format!("Failed to start {}: {error}", binary.display()))?; + let started = Instant::now(); + loop { + match child.try_wait() { + Ok(Some(_)) => { + return child + .wait_with_output() + .map_err(|error| format!("Failed to read the output of {}: {error}", binary.display())); + } + Ok(None) if started.elapsed() < timeout => std::thread::sleep(Duration::from_millis(50)), + Ok(None) => { + let _ = child.kill(); + let _ = child.wait(); + return Err(format!("{} did not finish within {} seconds.", binary.display(), timeout.as_secs())); + } + Err(error) => return Err(format!("Failed to wait for {}: {error}", binary.display())), + } + } +} + pub(super) fn install_command() -> &'static str { if cfg!(windows) { "powershell -NoProfile -Command \"irm https://dbxio.com/install-mcp.ps1 | iex\"" @@ -149,6 +209,31 @@ impl NativeInstallation { } } + /// Non-interactive read of the key, exactly what an MCP client's background + /// launch performs. Only meaningful on macOS, where Keychain items carry a + /// per-application ACL that a fresh binary has not been granted. + pub fn keychain_access(&self) -> KeychainAccess { + if !cfg!(target_os = "macos") { + return KeychainAccess::NotApplicable; + } + match run_with_timeout(&self.binary, VERIFY_KEYCHAIN_ARGUMENT, VERIFY_KEYCHAIN_TIMEOUT) { + Ok(output) => classify_keychain_probe(output.status.success(), &String::from_utf8_lossy(&output.stderr)), + Err(_) => KeychainAccess::Unknown, + } + } + + /// Runs the one-time `--authorize-keychain` recovery from this GUI process so + /// macOS can show its confirmation dialog. + pub fn authorize_keychain(&self) -> Result { + if !cfg!(target_os = "macos") { + return Err("Keychain authorization is only needed on macOS.".to_string()); + } + let output = run_with_timeout(&self.binary, AUTHORIZE_KEYCHAIN_ARGUMENT, AUTHORIZE_KEYCHAIN_TIMEOUT)?; + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + require_success(output, "dbx-mcp --authorize-keychain")?; + Ok(stdout) + } + pub fn install_command(&self) -> &'static str { match self.kind { NativeInstallationKind::Installer => install_command(), @@ -329,6 +414,50 @@ mod tests { ); } + #[test] + fn keychain_probe_distinguishes_denied_missing_and_unrecognized() { + assert_eq!(classify_keychain_probe(true, ""), KeychainAccess::Authorized); + assert_eq!( + classify_keychain_probe( + false, + "Error: \"KEYRING_ACCESS_FAILED: existing DBX Keychain key cannot be authorized\"" + ), + KeychainAccess::NeedsAuthorization + ); + assert_eq!( + classify_keychain_probe(false, "Error: \"SECRET_KEY_UNAVAILABLE: no existing DBX Keychain key\""), + KeychainAccess::KeyMissing + ); + // A binary that predates the probe flag rejects it as an unknown argument. + assert_eq!( + classify_keychain_probe(false, "Error: \"unknown DBX MCP argument: --verify-keychain-access\""), + KeychainAccess::Unknown + ); + } + + #[cfg(unix)] + #[test] + fn keychain_probe_runner_captures_output_and_enforces_the_deadline() { + use std::os::unix::fs::PermissionsExt; + let directory = tempfile::tempdir().unwrap(); + let failing = directory.path().join("failing"); + std::fs::write(&failing, "#!/bin/sh\necho KEYRING_ACCESS_FAILED >&2\nexit 1\n").unwrap(); + std::fs::set_permissions(&failing, std::fs::Permissions::from_mode(0o755)).unwrap(); + let output = run_with_timeout(&failing, VERIFY_KEYCHAIN_ARGUMENT, Duration::from_secs(5)).unwrap(); + assert!(!output.status.success()); + assert_eq!( + classify_keychain_probe(output.status.success(), &String::from_utf8_lossy(&output.stderr)), + KeychainAccess::NeedsAuthorization + ); + + let stalled = directory.path().join("stalled"); + std::fs::write(&stalled, "#!/bin/sh\nexec sleep 30\n").unwrap(); + std::fs::set_permissions(&stalled, std::fs::Permissions::from_mode(0o755)).unwrap(); + let started = Instant::now(); + assert!(run_with_timeout(&stalled, VERIFY_KEYCHAIN_ARGUMENT, Duration::from_millis(300)).is_err()); + assert!(started.elapsed() < Duration::from_secs(5)); + } + #[test] fn parses_version_output_and_rejects_marker_commands() { assert_eq!(parse_version("dbx-mcp 0.4.96\n").as_deref(), Some("0.4.96")); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 67b1b80dd9..b64507e331 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2830,6 +2830,8 @@ pub fn run() { commands::task_history::load_task_run, commands::task_history::load_task_run_items, commands::mcp::check_mcp_server_status, + commands::mcp::check_mcp_keychain_access, + commands::mcp::authorize_mcp_keychain, commands::mcp::install_mcp_server, commands::mcp::install_native_mcp_server, commands::mcp::uninstall_mcp_server,