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(() => {