From 691ecd084bca7db398792cff5c073a9f236d1b02 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, 7 Sep 2026 02:18:28 +0800 Subject: [PATCH] feat(mcp): add resource group scopes with permission inheritance --- .github/workflows/notify.yml | 6 +- .../editor/EditorSettingsDialog.vue | 1411 +++++++++-------- .../settings/McpAuthorizationStepper.vue | 20 +- .../settings/McpDatabaseScopePicker.vue | 153 +- .../settings/McpResourceScopePicker.vue | 354 +++++ apps/desktop/src/i18n/locales/en.ts | 60 +- apps/desktop/src/i18n/locales/es.ts | 60 +- apps/desktop/src/i18n/locales/it.ts | 56 +- apps/desktop/src/i18n/locales/ja.ts | 56 +- apps/desktop/src/i18n/locales/ko.ts | 56 +- apps/desktop/src/i18n/locales/pt-BR.ts | 56 +- apps/desktop/src/i18n/locales/tr.ts | 56 +- apps/desktop/src/i18n/locales/zh-CN.ts | 60 +- apps/desktop/src/i18n/locales/zh-TW.ts | 56 +- .../__tests__/mcp/mcpPolicySelection.spec.ts | 27 +- apps/desktop/src/lib/backend/tauri.ts | 8 + apps/desktop/src/lib/sidebar/sidebarLayout.ts | 18 + .../stores/__tests__/settingsStore.spec.ts | 8 + apps/desktop/src/stores/settingsStore.ts | 29 + crates/dbx-core/src/mcp_policy.rs | 111 ++ crates/dbx-core/src/query.rs | 10 +- crates/dbx-core/src/schema.rs | 8 +- crates/dbx-core/src/storage.rs | 91 +- crates/dbx-core/src/table_import.rs | 2 +- crates/dbx-mcp/src/backend.rs | 151 +- crates/dbx-mcp/src/server.rs | 146 +- crates/dbx-mcp/tests/live_batch.rs | 2 + crates/dbx-web/src/routes/connection.rs | 10 +- crates/dbx-web/src/routes/mcp_policy.rs | 70 +- docs/mcp-management-layout-redesign.md | 221 +++ src-tauri/src/commands/mcp_bridge.rs | 73 +- 31 files changed, 2398 insertions(+), 1047 deletions(-) create mode 100644 apps/desktop/src/components/settings/McpResourceScopePicker.vue create mode 100644 docs/mcp-management-layout-redesign.md diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index 47e6d2359a..d8df7861f1 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -195,7 +195,9 @@ jobs: PAYLOAD=$(jq -nc --arg msg "$MSG_CONTENT" \ '{"umo": "default:GroupMessage:1087880322", "message": $msg}') - printf '%s' "$PAYLOAD" | ssh "root@${HOST}" "curl -fsS -X POST http://localhost:6185/api/v1/im/message \ + if ! printf '%s' "$PAYLOAD" | ssh "root@${HOST}" "curl -fsS -X POST http://localhost:6185/api/v1/im/message \ -H 'Authorization: Bearer ${ASTRBOT_API_KEY}' \ -H 'Content-Type: application/json' \ - --data-binary @-" + --data-binary @-"; then + echo "::warning::QQ group notification failed; the event notification was sent to Feishu." + fi diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 1d818190c6..24264328ac 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -10,8 +10,6 @@ import { Check, CheckCircle2, ChevronDown, - ChevronLeft, - ChevronRight, ChevronUp, CircleHelp, Cloud, @@ -96,6 +94,7 @@ import { type CustomThemeColors, type CustomTheme, type McpConnectionPolicy, + type McpGroupPolicy, type ClickTableNavigationTarget, type SqlCompletionTriggerMode, SIDEBAR_INDENT_MIN, @@ -187,7 +186,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 McpResourceScopePicker from "@/components/settings/McpResourceScopePicker.vue"; import McpDatabaseScopePicker from "@/components/settings/McpDatabaseScopePicker.vue"; import McpAuthorizationStepper from "@/components/settings/McpAuthorizationStepper.vue"; import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue"; @@ -213,6 +212,7 @@ import type { PromptTemplate } from "@/types/promptTemplate"; import { GLOBAL_INSTRUCTIONS_MAX, PROMPT_TEMPLATE_CONTENT_MAX, PROMPT_TEMPLATE_NAME_MAX, promptTemplateCharacterCount } from "@/types/promptTemplate"; import { METADATA_CACHE_HARD_MAX_MEMORY_MB, METADATA_CACHE_MIN_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache"; import { databaseManifestEntry, manifestDatabaseTypes } from "@/lib/database/databaseDriverManifest"; +import { buildConnectionGroupIdPathMap, connectionGroupDestinationRows, connectionIdsInGroups } from "@/lib/sidebar/sidebarLayout"; const { t } = useI18n(); const { toast } = useToast(); @@ -2126,6 +2126,7 @@ async function exportDebugLogs() { type McpConfigTab = "claude" | "cursor" | "codebuddy" | "zcode" | "trae" | "vscode" | "windsurf" | "codex" | "deepseek-harness" | "opencode" | "pi" | "cherry-studio" | "qoder"; type McpCopyKind = "install" | "uninstall" | "http-endpoint" | "http-token" | "http-config" | `${McpConfigTab}-config`; type McpTransportTab = "stdio" | "http"; +type McpManagementTab = "access" | "permissions"; const mcpStatus = ref(null); const mcpStatusLoading = ref(false); @@ -2133,6 +2134,7 @@ const mcpStatusError = ref(""); const mcpCopied = ref<"" | McpCopyKind>(""); const mcpConfigTab = ref("claude"); const mcpTransportTab = ref("stdio"); +const mcpManagementTab = ref("access"); const mcpHttpSettings = ref({ enabled: false, host: "127.0.0.1", @@ -2162,6 +2164,7 @@ const mcpInstallError = ref(false); const mcpExecutionMode = computed(() => mcpExecutionModeFromPolicy(settingsStore.mcpGlobalPolicy)); const mcpExecutionModeOptions: McpExecutionMode[] = ["read_only", "safe_write", "high_risk_write"]; const mcpAllowedConnectionIds = computed(() => settingsStore.mcpGlobalPolicy.allowedConnectionIds); +const mcpAllowedGroupIds = computed(() => settingsStore.mcpGlobalPolicy.allowedGroupIds); const mcpQueryTimeoutInput = ref(settingsStore.mcpGlobalPolicy.queryTimeoutSecs === null ? "" : String(settingsStore.mcpGlobalPolicy.queryTimeoutSecs)); watch( @@ -2188,6 +2191,13 @@ function onMcpQueryTimeoutInput() { void saveMcpPolicy({ queryTimeoutSecs: parsed }); } const mcpSelectableConnections = computed(() => connectionStore.connections); +const mcpGroupRows = computed(() => connectionGroupDestinationRows(connectionStore.sidebarLayout)); +const mcpConnectionGroupIdPaths = computed(() => buildConnectionGroupIdPathMap(connectionStore.sidebarLayout)); +const mcpConnectionsInheritedFromGroups = computed(() => new Set(connectionIdsInGroups(connectionStore.sidebarLayout, mcpAllowedGroupIds.value))); +const mcpEffectiveAllowedConnectionIds = computed(() => { + if (mcpAllowedConnectionIds.value === null) return null; + return [...new Set([...mcpAllowedConnectionIds.value, ...mcpConnectionsInheritedFromGroups.value])]; +}); const mcpPolicyControlsDisabled = computed(() => isMcpPolicyMutationBlocked({ loading: mcpPolicyLoading.value, @@ -2237,6 +2247,7 @@ async function saveMcpPolicy(partial: { readOnly?: boolean; allowDangerousSql?: boolean; allowedConnectionIds?: string[] | null; + allowedGroupIds?: string[]; allowedToolNames?: string[] | null; connectionPolicies?: { connectionId: string; @@ -2248,6 +2259,7 @@ async function saveMcpPolicy(partial: { allowedDatabases: string[]; databasePolicies: { databaseName: string; readOnly: boolean; allowDangerousSql: boolean }[]; }[]; + groupPolicies?: McpGroupPolicy[]; queryTimeoutSecs?: number | null; }) { if (mcpPolicyControlsDisabled.value) return; @@ -2289,8 +2301,8 @@ function onMcpExecutionModeKeydown(event: KeyboardEvent, mode: McpExecutionMode) onMcpExecutionModeChange(nextMode); } -function onMcpAllowedConnectionIdsChange(allowedConnectionIds: string[] | null) { - void saveMcpPolicy({ allowedConnectionIds }); +function onMcpResourceScopeChange(scope: { allowedGroupIds: string[]; allowedConnectionIds: string[] | null }) { + void saveMcpPolicy(scope); } type McpConnectionExecutionMode = "read_only" | "safe_write" | "high_risk_write"; @@ -2328,28 +2340,9 @@ function onMcpToolAllowedChange(name: string, allowed: boolean) { } const mcpConnectionPolicyConnections = computed(() => { - const allowed = mcpAllowedConnectionIds.value; + const allowed = mcpEffectiveAllowedConnectionIds.value; return allowed === null ? mcpSelectableConnections.value : mcpSelectableConnections.value.filter((connection) => allowed.includes(connection.id)); }); -const MCP_CONNECTION_POLICY_PAGE_SIZE_OPTIONS = [6, 10, 20] as const; -const mcpConnectionPolicyPageSize = ref(MCP_CONNECTION_POLICY_PAGE_SIZE_OPTIONS[0]); -const mcpConnectionPolicyPage = ref(1); -const mcpConnectionPolicyPageCount = computed(() => Math.max(1, Math.ceil(mcpConnectionPolicyConnections.value.length / mcpConnectionPolicyPageSize.value))); -const pagedMcpConnectionPolicyConnections = computed(() => { - const start = (mcpConnectionPolicyPage.value - 1) * mcpConnectionPolicyPageSize.value; - return mcpConnectionPolicyConnections.value.slice(start, start + mcpConnectionPolicyPageSize.value); -}); - -function setMcpConnectionPolicyPage(page: number) { - mcpConnectionPolicyPage.value = Math.min(Math.max(1, page), mcpConnectionPolicyPageCount.value); -} - -watch(mcpConnectionPolicyPageSize, () => { - mcpConnectionPolicyPage.value = 1; -}); - -watch(mcpConnectionPolicyPageCount, () => setMcpConnectionPolicyPage(mcpConnectionPolicyPage.value)); - function mcpConnectionExecutionMode(connectionId: string): McpConnectionExecutionMode | "inherit" { const rule = settingsStore.mcpGlobalPolicy.connectionPolicies.find((item) => item.connectionId === connectionId); if (!rule || !rule.executionModeConfigured) return "inherit"; @@ -2357,14 +2350,39 @@ function mcpConnectionExecutionMode(connectionId: string): McpConnectionExecutio return rule.allowDangerousSql ? "high_risk_write" : "safe_write"; } +function mcpGroupExecutionMode(groupId: string): McpConnectionExecutionMode | "inherit" { + const rule = settingsStore.mcpGlobalPolicy.groupPolicies.find((item) => item.groupId === groupId); + if (!rule) return "inherit"; + if (rule.readOnly) return "read_only"; + return rule.allowDangerousSql ? "high_risk_write" : "safe_write"; +} + +function mcpInheritedGroupPolicy(connectionId: string): { mode: McpConnectionExecutionMode | "inherit"; source: string } { + const path = mcpConnectionGroupIdPaths.value.get(connectionId) ?? []; + for (let index = path.length - 1; index >= 0; index -= 1) { + const mode = mcpGroupExecutionMode(path[index]); + if (mode !== "inherit") { + const group = mcpGroupRows.value.find((item) => item.id === path[index]); + return { mode, source: group?.path.join(" / ") ?? path[index] }; + } + } + return { mode: "inherit", source: "" }; +} + +function mcpInheritedGroupExecutionMode(connectionId: string): McpConnectionExecutionMode | "inherit" { + return mcpInheritedGroupPolicy(connectionId).mode; +} + 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 groupMode = mcpInheritedGroupExecutionMode(policy.connectionId); + const inheritedMode = groupMode === "inherit" ? mcpExecutionMode.value : groupMode; + const legacyMode = connectionMode === "inherit" ? inheritedMode : connectionMode; + const effectiveMode = mcpExecutionModeRank(legacyMode) < mcpExecutionModeRank(inheritedMode) ? legacyMode : inheritedMode; 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; @@ -2386,8 +2404,15 @@ 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; +function mcpEffectiveExecutionMode(groupMode: McpConnectionExecutionMode | "inherit", connectionMode: McpConnectionExecutionMode | "inherit", databaseMode: McpConnectionExecutionMode | "inherit"): McpConnectionExecutionMode { + return databaseMode !== "inherit" ? databaseMode : connectionMode !== "inherit" ? connectionMode : groupMode !== "inherit" ? groupMode : mcpExecutionMode.value; +} + +function mcpPermissionSource(row: { databaseMode: McpConnectionExecutionMode | "inherit"; connectionMode: McpConnectionExecutionMode | "inherit"; groupMode: McpConnectionExecutionMode | "inherit"; groupSource: string }): string { + if (row.databaseMode !== "inherit") return t("settings.mcpPermissionSourceDatabase"); + if (row.connectionMode !== "inherit") return t("settings.mcpPermissionSourceConnection"); + if (row.groupMode !== "inherit") return t("settings.mcpPermissionSourceGroup", { group: row.groupSource }); + return t("settings.mcpPermissionSourceGlobal"); } const mcpPermissionPreviewSearchQuery = ref(""); @@ -2395,17 +2420,22 @@ const mcpPermissionPreviewRows = computed(() => mcpConnectionPolicyConnections.value.flatMap((connection) => { const rule = settingsStore.mcpGlobalPolicy.connectionPolicies.find((item) => item.connectionId === connection.id); const connectionMode = mcpConnectionExecutionMode(connection.id); + const groupPolicy = mcpInheritedGroupPolicy(connection.id); + const groupMode = groupPolicy.mode; 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 { + connectionId: connection.id, connection: connection.name, database, + groupMode, + groupSource: groupPolicy.source, connectionMode, databaseMode, - effectiveMode: mcpEffectiveExecutionMode(connectionMode, databaseMode), + effectiveMode: mcpEffectiveExecutionMode(groupMode, connectionMode, databaseMode), }; }); }), @@ -2413,10 +2443,24 @@ const mcpPermissionPreviewRows = computed(() => 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))); + return mcpPermissionPreviewRows.value.filter((row) => [row.connection, row.database, row.groupSource].some((value) => value.toLocaleLowerCase().includes(query))); }); +function onMcpGroupExecutionModeChange(groupId: string, mode: McpConnectionExecutionMode | "inherit") { + if (mode === "high_risk_write" && !window.confirm(t("settings.mcpGroupPolicyHighRiskConfirm"))) return; + const groupPolicies = settingsStore.mcpGlobalPolicy.groupPolicies.filter((item) => item.groupId !== groupId); + if (mode !== "inherit") { + groupPolicies.push({ + groupId, + readOnly: mode === "read_only", + allowDangerousSql: mode === "high_risk_write", + }); + } + void saveMcpPolicy({ groupPolicies }); +} + function onMcpConnectionExecutionModeChange(connectionId: string, mode: McpConnectionExecutionMode | "inherit") { + if (mode === "high_risk_write" && !window.confirm(t("settings.mcpExecutionModeHighRiskConfirm"))) return; 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; @@ -2439,7 +2483,27 @@ function onMcpConnectionExecutionModeChange(connectionId: string, mode: McpConne allowedDatabases: existing?.allowedDatabases ?? [], databasePolicies: selectedMode.databasePolicies, }; - if (next.executionModeConfigured || next.databaseScope !== "all") rules.push(next); + if (next.executionModeConfigured || next.databaseScope !== "all" || next.databasePolicies.length > 0) rules.push(next); + void saveMcpPolicy({ connectionPolicies: rules }); +} + +function onMcpDatabaseExecutionModeChange(connectionId: string, databaseName: string, mode: McpConnectionExecutionMode | "inherit") { + if (mode === "high_risk_write" && !window.confirm(t("settings.mcpDatabasePolicyHighRiskConfirm"))) return; + const existing = settingsStore.mcpGlobalPolicy.connectionPolicies.find((item) => item.connectionId === connectionId); + if (!existing || existing.databaseScope !== "selected" || !existing.allowedDatabases.includes(databaseName)) return; + const migrated = existing.executionModePolicyVersion === 1 ? null : migrateLegacyMcpConnectionPolicy(existing); + const databasePolicies = (migrated?.databasePolicies ?? existing.databasePolicies).filter((policy) => policy.databaseName !== databaseName); + if (mode !== "inherit") { + databasePolicies.push({ databaseName, readOnly: mode === "read_only", allowDangerousSql: mode === "high_risk_write" }); + } + const next = { + ...existing, + ...(migrated ?? {}), + executionModePolicyVersion: 1, + databasePolicies, + }; + const rules = settingsStore.mcpGlobalPolicy.connectionPolicies.filter((item) => item.connectionId !== connectionId); + if (next.executionModeConfigured || next.databaseScope !== "all" || next.databasePolicies.length > 0) rules.push(next); void saveMcpPolicy({ connectionPolicies: rules }); } @@ -7822,7 +7886,7 @@ LIMIT 100;
-
+
@@ -7833,7 +7897,12 @@ LIMIT 100;
- + @@ -7842,489 +7911,368 @@ LIMIT 100;
- + - {{ t("settings.mcpTransportLocalStdio") }} - {{ t("settings.mcpTransportHttpService") }} + {{ t("settings.mcpManagementAccess") }} + {{ t("settings.mcpManagementPermissions") }} - -
-
-
- - {{ webMcpHttpStatus?.enabled ? t("settings.mcpHttpStatusLabelEnabled") : t("settings.mcpHttpStatusLabelDisabled") }} -
-

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

- - -
-
- -
-
-
-
-
- - - {{ mcpHttpStatus?.running ? t("settings.mcpHttpStatusLabelRunning") : mcpHttpSettings.enabled ? t("settings.mcpHttpStatusLabelPending") : t("settings.mcpHttpStatusLabelDisabled") }} - -
-

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

-
- -
-
- -