feat(mcp): detect and authorize macOS Keychain access for dbx-mcp

This commit is contained in:
t8y2
2026-10-16 18:42:35 +08:00
parent c8a16708ca
commit 5cecc55613
21 changed files with 367 additions and 1 deletions
@@ -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<McpServerStatus | null>(null);
const mcpStatusLoading = ref(false);
const mcpStatusError = ref("");
const mcpKeychainAccess = ref<McpKeychainAccess | null>(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<McpConfigTab>("claude");
const mcpTransportTab = ref<McpTransportTab>("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;</pre>
</TabsContent>
<TabsContent value="stdio" class="m-0 space-y-4">
<div
v-if="!isWeb && (mcpKeychainNeedsAuthorization || mcpKeychainMessage)"
class="space-y-2 rounded-md border px-3 py-2.5 text-xs"
:class="mcpKeychainMessage && !mcpKeychainMessageError ? 'border-green-500/40 bg-green-500/10 text-green-700 dark:text-green-300' : 'border-amber-500/40 bg-amber-500/10 text-amber-800 dark:text-amber-200'"
>
<template v-if="mcpKeychainNeedsAuthorization">
<div class="flex items-center gap-1.5 font-medium">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
{{ t("settings.mcpKeychainTitle") }}
</div>
<p class="leading-relaxed">{{ t("settings.mcpKeychainDescription") }}</p>
<div class="flex flex-wrap items-center gap-2">
<Button type="button" size="sm" :disabled="mcpKeychainAuthorizing" @click="authorizeMcpKeychainAccess">
<Loader2 v-if="mcpKeychainAuthorizing" class="mr-1 h-3 w-3 animate-spin" />
{{ mcpKeychainAuthorizing ? t("settings.mcpKeychainAuthorizing") : t("settings.mcpKeychainAuthorizeButton") }}
</Button>
<span class="text-[11px] opacity-80">{{ t("settings.mcpKeychainAlwaysAllowHint") }}</span>
</div>
</template>
<p v-if="mcpKeychainMessage" class="select-text leading-relaxed">{{ mcpKeychainMessage }}</p>
</div>
<div v-if="!isWeb" class="grid gap-3 sm:grid-cols-2">
<div class="rounded-md border p-3">
<div class="text-xs font-medium uppercase text-muted-foreground">
+9
View File
@@ -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",
+9
View File
@@ -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",
+9
View File
@@ -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",
+9
View File
@@ -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",
+10
View File
@@ -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",
+9
View File
@@ -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: "最新バージョン",
+9
View File
@@ -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: "최신 버전",
+10
View File
@@ -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",
+9
View File
@@ -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: "Последняя версия",
+9
View File
@@ -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: "เวอร์ชันล่าสุด",
+9
View File
@@ -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",
+9
View File
@@ -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: "最新版本",
+9
View File
@@ -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: "最新版本",
+2
View File
@@ -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");
+8
View File
@@ -5627,6 +5627,14 @@ export async function checkMcpServerStatus(): Promise<import("@/lib/backend/taur
};
}
export async function checkMcpKeychainAccess(): Promise<import("@/lib/backend/tauri").McpKeychainAccess> {
return "not_applicable";
}
export async function authorizeMcpKeychain(): Promise<string> {
throw new Error("MCP Keychain authorization is only available in the desktop app.");
}
export async function installMcpServer(): Promise<string> {
throw new Error("MCP Server installation is only available in the desktop app.");
}
+10
View File
@@ -3416,6 +3416,16 @@ export async function checkMcpServerStatus(): Promise<McpServerStatus> {
return invoke("check_mcp_server_status");
}
export type McpKeychainAccess = "authorized" | "needs_authorization" | "key_missing" | "unknown" | "not_applicable";
export async function checkMcpKeychainAccess(): Promise<McpKeychainAccess> {
return invoke("check_mcp_keychain_access");
}
export async function authorizeMcpKeychain(): Promise<string> {
return invoke("authorize_mcp_keychain");
}
export async function installMcpServer(): Promise<string> {
return invoke("install_mcp_server");
}
+7
View File
@@ -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
+25
View File
@@ -363,6 +363,31 @@ pub async fn check_mcp_server_status(app: AppHandle) -> Result<McpServerStatus,
})
}
/// Whether a background `dbx-mcp` launch can read the DBX encryption key.
/// Kept apart from `check_mcp_server_status` so the frequent update-badge
/// polling does not spawn a probe process.
#[tauri::command]
pub async fn check_mcp_keychain_access() -> Result<native_installation::KeychainAccess, String> {
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<String, String> {
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<String, String> {
tauri::async_runtime::spawn_blocking(|| {
@@ -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<Output, String> {
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<String, String> {
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"));
+2
View File
@@ -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,