diff --git a/README.md b/README.md index 4eaa94822..22f55c2bc 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,10 @@ Add to your `.mcp.json`: } ``` +Manage the connection allowlist and the **Read only**, **Data read/write**, and **Full access** modes in **DBX Settings → MCP**. The machine-readable values remain `read_only`, `safe_write`, and `high_risk_write`; client configs do not need permission or connection-scope environment variables. + +For upgrade compatibility, an existing `DBX_MCP_ALLOW_WRITES=0` (or `false`) remains a read-only restriction only until a central MCP policy is saved for the first time; it can never enable writes or override a saved policy. + Windows portable builds need `DBX_DATA_DIR` in the MCP config, pointing to the `data` directory next to `DBX.exe` (the folder that contains `dbx.db`). For DBX Web or Docker deployments, point the MCP server at the Web backend API. If the Web login page requires a password, set `DBX_WEB_PASSWORD` to the same password used there: diff --git a/README.zh-CN.md b/README.zh-CN.md index 68e8e77c0..9165b4700 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -156,6 +156,10 @@ npx @dbx-app/mcp-server } ``` +连接 allowlist 和“只读 / 数据读写 / 完全访问”三档执行权限统一在 DBX 的“设置 → MCP”中管理。机器可读值仍为 `read_only`、`safe_write`、`high_risk_write`;客户端配置无需声明权限或连接范围环境变量。 + +为兼容升级,旧配置中的 `DBX_MCP_ALLOW_WRITES=0`(或 `false`)仅在中央 MCP 策略首次保存前继续作为只读限制;它不能开启写入,也不能覆盖已经保存的中央策略。 + Windows 便携版需要在 MCP 配置中设置 `DBX_DATA_DIR`,指向 `DBX.exe` 同级的 `data` 目录(即包含 `dbx.db` 的文件夹)。 如果连接的是 DBX Web 或 Docker 部署,请让 MCP Server 指向 Web 后端 API。如果 Web 登录页需要密码,`DBX_WEB_PASSWORD` 填写同一个 Web 登录密码: diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 380f98b69..7b968df1c 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -95,7 +95,8 @@ import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sq import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache"; import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates"; import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, normalizeSqlVariableSyntaxOverrides, SQL_VARIABLE_SYNTAX_DATABASE_TYPES, SQL_VARIABLE_SYNTAX_KEYS, SQL_VARIABLE_SYNTAX_TOKENS, type SqlVariableSyntaxOverrides, type SqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; -import { buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, type McpEnvEntry, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; +import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; +import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection"; import { isMacOS } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; import { useToast } from "@/composables/useToast"; @@ -105,6 +106,7 @@ import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; import AiProviderLogo from "@/components/icons/AiProviderLogo.vue"; import AppLogo from "@/components/icons/AppLogo.vue"; import ChangelogPanel from "@/components/settings/ChangelogPanel.vue"; +import McpConnectionScopePicker from "@/components/settings/McpConnectionScopePicker.vue"; import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue"; import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue"; import { APP_THEME_PALETTES, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme"; @@ -1324,7 +1326,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[ { value: "snippets", label: t("settings.snippetsTab") }, ...(isWeb ? [] : [{ value: "sync" as const, label: t("settings.syncTab") }]), { value: "ai", label: t("settings.aiTab") }, - ...(isWeb ? [] : [{ value: "mcp" as const, label: t("settings.mcpTab") }]), + { value: "mcp" as const, label: t("settings.mcpTab") }, ...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []), { value: "about", label: t("settings.aboutTab") }, ]); @@ -1411,7 +1413,7 @@ async function exportDebugLogs() { } // ---------- MCP Server ---------- -type McpConfigTab = "claude" | "cursor" | "trae" | "vscode" | "windsurf" | "codex" | "opencode"; +type McpConfigTab = "claude" | "cursor" | "trae" | "vscode" | "windsurf" | "codex" | "opencode" | "cherry-studio"; type McpCopyKind = "install" | `${McpConfigTab}-config`; const mcpStatus = ref(null); @@ -1420,39 +1422,80 @@ const mcpStatusError = ref(""); const mcpCopied = ref<"" | McpCopyKind>(""); const mcpConfigTab = ref("claude"); const MCP_READONLY_STORAGE_KEY = "dbx-mcp-config-readonly"; -const MCP_ALLOW_DANGEROUS_STORAGE_KEY = "dbx-mcp-config-allow-dangerous"; -const mcpReadonlyMode = ref(localStorage.getItem(MCP_READONLY_STORAGE_KEY) === "true"); -const mcpAllowDangerous = ref(localStorage.getItem(MCP_ALLOW_DANGEROUS_STORAGE_KEY) === "true"); +const MCP_SCOPE_CONNECTION_STORAGE_KEY = "dbx-mcp-config-scope-connection"; +const mcpPolicyLoading = ref(false); +const mcpPolicySaving = ref(false); +const mcpPolicyLoadError = ref(""); const mcpInstalling = ref(false); const mcpInstallMessage = ref(""); const mcpInstallError = ref(false); +const mcpExecutionMode = computed(() => mcpExecutionModeFromPolicy(settingsStore.mcpGlobalPolicy)); +const mcpAllowedConnectionIds = computed(() => settingsStore.mcpGlobalPolicy.allowedConnectionIds); +const mcpSelectableConnections = computed(() => connectionStore.connections); +const mcpPolicyControlsDisabled = computed(() => + isMcpPolicyMutationBlocked({ + loading: mcpPolicyLoading.value, + saving: mcpPolicySaving.value, + loadError: mcpPolicyLoadError.value, + }), +); -const mcpEnvEntries = computed(() => { - const entries: McpEnvEntry[] = []; - if (mcpReadonlyMode.value) { - entries.push(["DBX_MCP_ALLOW_WRITES", "0"]); +async function saveMcpPolicy(partial: { readOnly?: boolean; allowDangerousSql?: boolean; allowedConnectionIds?: string[] | null }) { + if (mcpPolicyControlsDisabled.value) return; + mcpPolicySaving.value = true; + try { + await settingsStore.updateMcpGlobalPolicy(partial); + } catch (e: any) { + toast(t("settings.mcpPolicySaveFailed", { error: e?.message || String(e) }), 5000); + } finally { + mcpPolicySaving.value = false; } - if (!mcpReadonlyMode.value && mcpAllowDangerous.value) { - entries.push(["DBX_MCP_ALLOW_DANGEROUS_SQL", "1"]); +} + +function onMcpExecutionModeChange(event: Event, mode: McpExecutionMode) { + if (mode === mcpExecutionMode.value) return; + if (mode === "high_risk_write" && !window.confirm(t("settings.mcpExecutionModeHighRiskConfirm"))) { + event.preventDefault(); + return; } - return entries; -}); + void saveMcpPolicy(mcpPolicyFieldsForExecutionMode(mode)); +} + +function onMcpAllowedConnectionIdsChange(allowedConnectionIds: string[] | null) { + void saveMcpPolicy({ allowedConnectionIds }); +} const mcpLaunchConfig = computed(() => { - if (!mcpStatus.value?.node_path || !mcpStatus.value.script_path) return undefined; - return { - command: mcpStatus.value.node_path, - args: [mcpStatus.value.script_path], - }; + if (isWeb) { + return { + command: "dbx-mcp-server", + env: { + DBX_WEB_URL: mcpWebBackendUrl(window.location.origin, apiUrl("/api")), + DBX_WEB_PASSWORD: "your-web-login-password", + }, + }; + } + if (mcpStatus.value?.node_path && mcpStatus.value.script_path) { + return { + command: mcpStatus.value.node_path, + args: [mcpStatus.value.script_path], + }; + } + if (mcpStatus.value?.bin_path) { + return { command: mcpStatus.value.bin_path }; + } + return undefined; }); -const mcpJsonRecommendedConfig = computed(() => buildMcpJsonConfig(mcpEnvEntries.value, mcpLaunchConfig.value)); +const mcpJsonRecommendedConfig = computed(() => buildMcpJsonConfig(mcpLaunchConfig.value)); -const mcpVsCodeRecommendedConfig = computed(() => buildMcpVsCodeConfig(mcpEnvEntries.value, mcpLaunchConfig.value)); +const mcpVsCodeRecommendedConfig = computed(() => buildMcpVsCodeConfig(mcpLaunchConfig.value)); -const mcpCodexRecommendedConfig = computed(() => buildMcpCodexConfig(mcpEnvEntries.value, mcpLaunchConfig.value)); +const mcpCherryStudioRecommendedConfig = computed(() => buildMcpCherryStudioConfig(mcpLaunchConfig.value)); -const mcpOpenCodeRecommendedConfig = computed(() => buildMcpOpenCodeConfig(mcpEnvEntries.value, mcpLaunchConfig.value)); +const mcpCodexRecommendedConfig = computed(() => buildMcpCodexConfig(mcpLaunchConfig.value)); + +const mcpOpenCodeRecommendedConfig = computed(() => buildMcpOpenCodeConfig(mcpLaunchConfig.value)); const mcpStatusTone = computed<"ok" | "warning" | "muted">(() => { if (!mcpStatus.value) return "muted"; @@ -1474,15 +1517,6 @@ const mcpCommand = computed(() => { return mcpStatus.value.installed ? mcpStatus.value.update_command : mcpStatus.value.install_command; }); -watch(mcpReadonlyMode, (value) => { - localStorage.setItem(MCP_READONLY_STORAGE_KEY, String(value)); - if (value) mcpAllowDangerous.value = false; -}); - -watch(mcpAllowDangerous, (value) => { - localStorage.setItem(MCP_ALLOW_DANGEROUS_STORAGE_KEY, String(value)); -}); - async function refreshMcpStatus() { if (mcpStatusLoading.value) return; mcpStatusLoading.value = true; @@ -1821,6 +1855,8 @@ watch( () => settingsVisible.value, async (open) => { if (open) { + mcpPolicyLoading.value = true; + mcpPolicyLoadError.value = ""; aiConfigListMode.value = "list"; aiEditConfigId.value = null; activeSettingsTab.value = props.initialTab || "appearance"; @@ -1828,6 +1864,19 @@ watch( oldPassword.value = ""; newPassword.value = ""; confirmNewPassword.value = ""; + try { + await settingsStore.initMcpGlobalPolicy(true); + if (!settingsStore.mcpGlobalPolicy.configured && localStorage.getItem(MCP_READONLY_STORAGE_KEY) === "true") { + await settingsStore.updateMcpGlobalPolicy({ readOnly: true }); + } + if (settingsStore.mcpGlobalPolicy.configured) localStorage.removeItem(MCP_READONLY_STORAGE_KEY); + localStorage.removeItem(MCP_SCOPE_CONNECTION_STORAGE_KEY); + } catch (e: any) { + mcpPolicyLoadError.value = e?.message || String(e); + toast(t("settings.mcpPolicyLoadFailed", { error: mcpPolicyLoadError.value }), 5000); + } finally { + mcpPolicyLoading.value = false; + } await settingsStore.initAiConfigs(); await settingsStore.initDesktopSettings(); editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon; @@ -4751,7 +4800,7 @@ onUnmounted(cleanupPreviewEditor); -
+
@@ -4763,7 +4812,7 @@ onUnmounted(cleanupPreviewEditor);
- + @@ -4772,7 +4821,7 @@ onUnmounted(cleanupPreviewEditor);
-
+
{{ t("settings.mcpCurrent") }}
@@ -4806,7 +4855,7 @@ onUnmounted(cleanupPreviewEditor);
-
+
@@ -4832,36 +4881,101 @@ onUnmounted(cleanupPreviewEditor);

{{ t("settings.mcpConfigOptionsHint") }}

-
+

{{ t("settings.mcpPolicyLoadFailed", { error: mcpPolicyLoadError }) }}

+ +
- -

{{ t("settings.mcpReadonlyModeDescription") }}

+ +

{{ t("settings.mcpExecutionModeDescription") }}

- -
-
- -
-
-
- -

{{ t("settings.mcpAllowDangerousDescription") }}

+
+ {{ t("settings.mcpExecutionMode") }} +
+ + + +
+
+

+ + {{ t("settings.mcpExecutionModeReadOnlyDescription") }} + {{ t("settings.mcpExecutionModeSafeWriteDescription") }} + {{ t("settings.mcpExecutionModeHighRiskWriteDescription") }} +

+
+
+

{{ t("settings.mcpCapabilityTitle") }}

+

{{ t("settings.mcpCapabilityDescription") }}

+
+
+ + + + + + + + + + + + + +
{{ t("settings.mcpCapabilityOperation") }} + {{ t(column.labelKey) }} +
{{ t(row.labelKey) }} + + +
+
+

{{ t("settings.mcpCapabilityAlwaysEnforced") }}

-
- - Claude Code - Cursor - TRAE - VS Code - Windsurf - Codex - OpenCode + + Claude Code + Cursor + TRAE + VS Code + Windsurf + Codex + OpenCode + Cherry Studio @@ -4963,6 +5077,21 @@ onUnmounted(cleanupPreviewEditor);
+ + +
+
+ {{ t("settings.mcpCherryStudioConfigPath") }} +
+
+
{{ mcpCherryStudioRecommendedConfig }}
+ +
+
+
@@ -5165,12 +5294,12 @@ onUnmounted(cleanupPreviewEditor); - +
-