feat(mcp): override execution policy per database

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-02 17:39:07 +08:00
1 parent 1706081802
commit c48ebda197
24 files changed
+1279 -307

No files matched your search

@@ -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(() => {
</template>
<template #capabilities>
<div class="space-y-4">
<section class="space-y-2 rounded-md border bg-background p-3">
<div>
<p class="text-sm font-medium">{{ t("settings.mcpPermissionPreviewTitle") }}</p>
<p class="text-xs text-muted-foreground">{{ t("settings.mcpPermissionPreviewDescription") }}</p>
</div>
<template v-if="mcpPermissionPreviewRows.length">
<div class="relative">
<Search class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input v-model="mcpPermissionPreviewSearchQuery" autocomplete="off" :placeholder="t('settings.mcpPermissionPreviewSearchPlaceholder')" class="h-8 pl-9 text-xs" />
</div>
<div class="max-h-72 overflow-auto overscroll-contain rounded border">
<table class="w-full min-w-[42rem] border-separate border-spacing-0 text-xs">
<thead class="text-muted-foreground">
<tr>
<th scope="col" class="sticky top-0 z-10 bg-muted px-3 py-2 text-left font-medium shadow-[0_1px_0_hsl(var(--border))]">{{ t("settings.mcpPermissionPreviewConnection") }}</th>
<th scope="col" class="sticky top-0 z-10 bg-muted px-3 py-2 text-left font-medium shadow-[0_1px_0_hsl(var(--border))]">{{ t("settings.mcpPermissionPreviewDatabase") }}</th>
<th scope="col" class="sticky top-0 z-10 bg-muted px-3 py-2 text-left font-medium shadow-[0_1px_0_hsl(var(--border))]">{{ t("settings.mcpPermissionPreviewConnectionDefault") }}</th>
<th scope="col" class="sticky top-0 z-10 bg-muted px-3 py-2 text-left font-medium shadow-[0_1px_0_hsl(var(--border))]">{{ t("settings.mcpPermissionPreviewDatabaseOverride") }}</th>
<th scope="col" class="sticky top-0 z-10 bg-muted px-3 py-2 text-left font-medium shadow-[0_1px_0_hsl(var(--border))]">{{ t("settings.mcpPermissionPreviewEffective") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in filteredMcpPermissionPreviewRows" :key="`${row.connection}:${row.database}`" class="border-t">
<td class="px-3 py-2 font-medium">{{ row.connection }}</td>
<td class="px-3 py-2 font-mono">{{ row.database }}</td>
<td class="px-3 py-2">{{ row.connectionMode === "inherit" ? t("settings.mcpConnectionPolicyInherit") : mcpExecutionModeLabel(row.connectionMode) }}</td>
<td class="px-3 py-2">{{ row.databaseMode === "inherit" ? t("settings.mcpDatabasePolicyInherit") : mcpExecutionModeLabel(row.databaseMode) }}</td>
<td class="px-3 py-2 font-medium">{{ mcpExecutionModeLabel(row.effectiveMode) }}</td>
</tr>
<tr v-if="filteredMcpPermissionPreviewRows.length === 0">
<td colspan="5" class="px-3 py-8 text-center text-muted-foreground">{{ t("settings.mcpPermissionPreviewSearchNoResults") }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<p v-else class="text-xs text-muted-foreground">{{ t("settings.mcpPermissionPreviewEmpty") }}</p>
</section>
<section class="space-y-2">
<div>
<p class="text-sm font-medium">{{ t("settings.mcpToolPermissionsTitle") }}</p>