From c48ebda197092ff621af42b40907425dae945bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Wed, 2 Sep 2026 17:39:07 +0800 Subject: [PATCH] feat(mcp): override execution policy per database --- .../editor/EditorSettingsDialog.vue | 129 ++++++++++- .../settings/McpDatabaseScopePicker.vue | 113 +++++++++- apps/desktop/src/i18n/locales/en.ts | 29 ++- apps/desktop/src/i18n/locales/es.ts | 29 ++- apps/desktop/src/i18n/locales/it.ts | 29 ++- apps/desktop/src/i18n/locales/ja.ts | 29 ++- apps/desktop/src/i18n/locales/ko.ts | 29 ++- apps/desktop/src/i18n/locales/pt-BR.ts | 29 ++- apps/desktop/src/i18n/locales/zh-CN.ts | 29 ++- apps/desktop/src/i18n/locales/zh-TW.ts | 29 ++- apps/desktop/src/lib/backend/tauri.ts | 8 + .../stores/__tests__/settingsStore.spec.ts | 45 ++++ apps/desktop/src/stores/settingsStore.ts | 32 ++- crates/dbx-core/src/lib.rs | 1 + crates/dbx-core/src/mcp_policy.rs | 208 ++++++++++++++++++ crates/dbx-core/src/storage.rs | 95 +++++++- crates/dbx-mcp/src/server.rs | 192 ++++++++++------ crates/dbx-web/src/routes/mcp_policy.rs | 166 ++++++++++++-- crates/dbx-web/src/routes/mongo.rs | 145 ++++++------ crates/dbx-web/src/routes/query.rs | 50 +++-- crates/dbx-web/src/routes/vector.rs | 16 +- docs/content/docs/mcp.cn.mdx | 12 +- docs/content/docs/mcp.mdx | 12 +- src-tauri/src/commands/mcp_bridge.rs | 130 +++++++---- 24 files changed, 1279 insertions(+), 307 deletions(-) create mode 100644 crates/dbx-core/src/mcp_policy.rs diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 2b47a6a093..bcaeec06ac 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -90,6 +90,7 @@ import { type UpdateDownloadSource, type CustomThemeColors, type CustomTheme, + type McpConnectionPolicy, type ClickTableNavigationTarget, type SqlCompletionTriggerMode, SIDEBAR_INDENT_MIN, @@ -1926,6 +1927,7 @@ function resetSettingsSearchState() { settingsSearchActiveIndex.value = 0; pendingSettingsSearchResult = null; shortcutSearchQuery.value = ""; + mcpPermissionPreviewSearchQuery.value = ""; clearSettingsSearchHighlight(); } @@ -2219,7 +2221,16 @@ async function saveMcpPolicy(partial: { allowDangerousSql?: boolean; allowedConnectionIds?: string[] | null; allowedToolNames?: string[] | null; - connectionPolicies?: { connectionId: string; readOnly: boolean; allowDangerousSql: boolean; executionModeConfigured: boolean; databaseScope: "all" | "selected" | "none"; allowedDatabases: string[] }[]; + connectionPolicies?: { + connectionId: string; + readOnly: boolean; + allowDangerousSql: boolean; + executionModeConfigured: boolean; + executionModePolicyVersion: number | null; + databaseScope: "all" | "selected" | "none"; + allowedDatabases: string[]; + databasePolicies: { databaseName: string; readOnly: boolean; allowDangerousSql: boolean }[]; + }[]; queryTimeoutSecs?: number | null; }) { if (mcpPolicyControlsDisabled.value) return; @@ -2327,16 +2338,87 @@ function mcpConnectionExecutionMode(connectionId: string): McpConnectionExecutio return rule.allowDangerousSql ? "high_risk_write" : "safe_write"; } +function mcpExecutionModeRank(mode: McpConnectionExecutionMode): number { + return mode === "read_only" ? 0 : mode === "safe_write" ? 1 : 2; +} + +function migrateLegacyMcpConnectionPolicy(policy: McpConnectionPolicy) { + const connectionMode = mcpConnectionExecutionMode(policy.connectionId); + const legacyMode = connectionMode === "inherit" ? mcpExecutionMode.value : connectionMode; + const effectiveMode = mcpExecutionModeRank(legacyMode) < mcpExecutionModeRank(mcpExecutionMode.value) ? legacyMode : mcpExecutionMode.value; + const databasePolicies = policy.databasePolicies.map((databasePolicy) => { + const databaseMode: McpConnectionExecutionMode = databasePolicy.readOnly ? "read_only" : databasePolicy.allowDangerousSql ? "high_risk_write" : "safe_write"; + const effectiveDatabaseMode = mcpExecutionModeRank(databaseMode) < mcpExecutionModeRank(effectiveMode) ? databaseMode : effectiveMode; + return { + ...databasePolicy, + readOnly: effectiveDatabaseMode === "read_only", + allowDangerousSql: effectiveDatabaseMode === "high_risk_write", + }; + }); + return { + readOnly: effectiveMode === "read_only", + allowDangerousSql: effectiveMode === "high_risk_write", + executionModeConfigured: true, + databasePolicies, + }; +} + +function mcpExecutionModeLabel(mode: McpConnectionExecutionMode): string { + return t(mode === "read_only" ? "settings.mcpExecutionModeReadOnly" : mode === "safe_write" ? "settings.mcpExecutionModeSafeWrite" : "settings.mcpExecutionModeHighRiskWrite"); +} + +function mcpEffectiveExecutionMode(connectionMode: McpConnectionExecutionMode | "inherit", databaseMode: McpConnectionExecutionMode | "inherit"): McpConnectionExecutionMode { + return databaseMode !== "inherit" ? databaseMode : connectionMode !== "inherit" ? connectionMode : mcpExecutionMode.value; +} + +const mcpPermissionPreviewSearchQuery = ref(""); +const mcpPermissionPreviewRows = computed(() => + mcpConnectionPolicyConnections.value.flatMap((connection) => { + const rule = settingsStore.mcpGlobalPolicy.connectionPolicies.find((item) => item.connectionId === connection.id); + const connectionMode = mcpConnectionExecutionMode(connection.id); + if (rule?.databaseScope === "none") return []; + const databases = rule?.databaseScope === "selected" ? rule.allowedDatabases : [t("settings.mcpDatabaseScopeSummaryAll")]; + return databases.map((database) => { + const databasePolicy = rule?.databasePolicies.find((item) => item.databaseName === database); + const databaseMode: McpConnectionExecutionMode | "inherit" = !databasePolicy ? "inherit" : databasePolicy.readOnly ? "read_only" : databasePolicy.allowDangerousSql ? "high_risk_write" : "safe_write"; + return { + connection: connection.name, + database, + connectionMode, + databaseMode, + effectiveMode: mcpEffectiveExecutionMode(connectionMode, databaseMode), + }; + }); + }), +); +const filteredMcpPermissionPreviewRows = computed(() => { + const query = mcpPermissionPreviewSearchQuery.value.trim().toLocaleLowerCase(); + if (!query) return mcpPermissionPreviewRows.value; + return mcpPermissionPreviewRows.value.filter((row) => [row.connection, row.database].some((value) => value.toLocaleLowerCase().includes(query))); +}); + function onMcpConnectionExecutionModeChange(connectionId: string, mode: McpConnectionExecutionMode | "inherit") { const existing = settingsStore.mcpGlobalPolicy.connectionPolicies.find((item) => item.connectionId === connectionId); const rules = settingsStore.mcpGlobalPolicy.connectionPolicies.filter((item) => item.connectionId !== connectionId); + const migrated = existing && existing.executionModePolicyVersion !== 1 ? migrateLegacyMcpConnectionPolicy(existing) : null; + const selectedMode = + migrated && mode === "inherit" + ? migrated + : { + readOnly: mode === "read_only", + allowDangerousSql: mode === "high_risk_write", + executionModeConfigured: mode !== "inherit", + databasePolicies: migrated?.databasePolicies ?? existing?.databasePolicies ?? [], + }; const next = { connectionId, - readOnly: mode === "read_only", - allowDangerousSql: mode === "high_risk_write", - executionModeConfigured: mode !== "inherit", + readOnly: selectedMode.readOnly, + allowDangerousSql: selectedMode.allowDangerousSql, + executionModeConfigured: selectedMode.executionModeConfigured, + executionModePolicyVersion: 1, databaseScope: existing?.databaseScope ?? ("all" as const), allowedDatabases: existing?.allowedDatabases ?? [], + databasePolicies: selectedMode.databasePolicies, }; if (next.executionModeConfigured || next.databaseScope !== "all") rules.push(next); void saveMcpPolicy({ connectionPolicies: rules }); @@ -7893,6 +7975,7 @@ onUnmounted(() => { :connections="mcpSelectableConnections" :allowed-connection-ids="mcpAllowedConnectionIds" :connection-policies="settingsStore.mcpGlobalPolicy.connectionPolicies" + :global-execution-mode="mcpExecutionMode" :disabled="mcpPolicyControlsDisabled" :busy="mcpPolicyLoading || mcpPolicySaving" @update:connection-policies="onMcpConnectionPoliciesChange" @@ -7943,6 +8026,44 @@ onUnmounted(() => {