From d642d01eacd4558ccb8a4e41613a13c67ffdfd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 20 Jul 2026 22:47:40 +0800 Subject: [PATCH] feat(mcp): centralize connection access policies --- README.md | 4 + README.zh-CN.md | 4 + .../editor/EditorSettingsDialog.vue | 247 +++-- .../settings/McpConnectionScopePicker.vue | 380 +++++++ .../McpConnectionScopePicker.spec.ts | 89 ++ .../desktop/src/composables/useTauriEvents.ts | 13 +- apps/desktop/src/i18n/locales/en.ts | 62 +- apps/desktop/src/i18n/locales/es.ts | 63 +- apps/desktop/src/i18n/locales/it.ts | 63 +- apps/desktop/src/i18n/locales/ja.ts | 62 +- apps/desktop/src/i18n/locales/pt-BR.ts | 63 +- apps/desktop/src/i18n/locales/zh-CN.ts | 62 +- apps/desktop/src/i18n/locales/zh-TW.ts | 62 +- .../__tests__/mcp/mcpConfigTemplates.spec.ts | 90 +- .../__tests__/mcp/mcpPolicySelection.spec.ts | 117 +++ apps/desktop/src/lib/backend/api.ts | 2 + apps/desktop/src/lib/backend/http.ts | 14 + apps/desktop/src/lib/backend/tauri.ts | 15 + .../desktop/src/lib/mcp/mcpConfigTemplates.ts | 59 +- .../desktop/src/lib/mcp/mcpPolicySelection.ts | 100 ++ .../src/lib/mongo/mongoShellCommand.ts | 189 +++- .../stores/__tests__/settingsStore.spec.ts | 73 +- apps/desktop/src/stores/queryStore.ts | 22 + apps/desktop/src/stores/settingsStore.ts | 55 ++ crates/dbx-cli/src/main.rs | 18 +- crates/dbx-core/src/db/redis_driver.rs | 188 +++- crates/dbx-core/src/models/connection.rs | 18 + crates/dbx-core/src/mongo_shell.rs | 241 ++++- crates/dbx-core/src/production_safety.rs | 112 ++- crates/dbx-core/src/query_execution_sql.rs | 2 +- crates/dbx-core/src/sql_risk.rs | 924 +++++++++++++++++- crates/dbx-core/src/storage.rs | 501 ++++++++-- crates/dbx-mcp/src/backend.rs | 98 +- crates/dbx-mcp/src/server.rs | 568 ++++++++--- crates/dbx-mcp/tests/local.rs | 76 +- crates/dbx-mcp/tests/protocol.rs | 125 ++- crates/dbx-web/src/main.rs | 6 + crates/dbx-web/src/routes/app_settings.rs | 15 + crates/dbx-web/src/routes/connection.rs | 137 ++- crates/dbx-web/src/routes/mcp_policy.rs | 438 +++++++++ crates/dbx-web/src/routes/mod.rs | 1 + crates/dbx-web/src/routes/mongo.rs | 206 ++++ crates/dbx-web/src/routes/query.rs | 9 + crates/dbx-web/src/routes/redis.rs | 38 +- docs/content/docs/mcp.cn.mdx | 24 +- docs/content/docs/mcp.mdx | 24 +- .../2026-07-18-mcp-access-policy-handoff.md | 171 ++++ examples/README.md | 2 + packages/app-tests/mongoShellCommand.test.ts | 64 +- packages/mcp-server/README.md | 102 +- packages/mcp-server/tests/bin.test.ts | 6 +- src-tauri/src/commands/app_settings.rs | 12 +- src-tauri/src/commands/mcp_bridge.rs | 849 ++++++++++++++-- src-tauri/src/commands/mongo_cmd.rs | 114 +++ src-tauri/src/commands/redis_cmd.rs | 9 +- src-tauri/src/lib.rs | 2 + 56 files changed, 6393 insertions(+), 617 deletions(-) create mode 100644 apps/desktop/src/components/settings/McpConnectionScopePicker.vue create mode 100644 apps/desktop/src/components/settings/__tests__/McpConnectionScopePicker.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/mcp/mcpPolicySelection.spec.ts create mode 100644 apps/desktop/src/lib/mcp/mcpPolicySelection.ts create mode 100644 crates/dbx-web/src/routes/mcp_policy.rs create mode 100644 docs/pips/plans/2026-07-18-mcp-access-policy-handoff.md 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); - +
-