feat(mcp): override execution policy per database
This commit is contained in:
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>
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { McpConnectionPolicy } from "@/stores/settingsStore";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
type DatabaseScope = McpConnectionPolicy["databaseScope"];
|
||||
type DatabaseExecutionMode = "inherit" | "read_only" | "safe_write" | "high_risk_write";
|
||||
type ExecutionMode = Exclude<DatabaseExecutionMode, "inherit">;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -20,6 +22,7 @@ const props = withDefaults(
|
||||
connections: readonly ConnectionConfig[];
|
||||
allowedConnectionIds: readonly string[] | null;
|
||||
connectionPolicies: readonly McpConnectionPolicy[];
|
||||
globalExecutionMode: ExecutionMode;
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
}>(),
|
||||
@@ -45,6 +48,7 @@ const selectedConnection = computed(() => scopedConnections.value.find((connecti
|
||||
const selectedPolicy = computed(() => props.connectionPolicies.find((policy) => policy.connectionId === selectedConnectionId.value));
|
||||
const selectedScope = computed<DatabaseScope>(() => selectedPolicy.value?.databaseScope ?? "all");
|
||||
const selectedDatabases = computed(() => selectedPolicy.value?.allowedDatabases ?? []);
|
||||
const selectedDatabasePolicies = computed(() => selectedPolicy.value?.databasePolicies ?? []);
|
||||
const displayedDatabases = computed(() => {
|
||||
const query = search.value.trim().toLocaleLowerCase();
|
||||
return (databasesByConnection.value[selectedConnectionId.value] ?? []).filter((database) => !query || database.toLocaleLowerCase().includes(query));
|
||||
@@ -77,21 +81,29 @@ function policyFor(connectionId: string): McpConnectionPolicy {
|
||||
readOnly: false,
|
||||
allowDangerousSql: false,
|
||||
executionModeConfigured: false,
|
||||
executionModePolicyVersion: null,
|
||||
databaseScope: "all",
|
||||
allowedDatabases: [],
|
||||
databasePolicies: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateSelectedPolicy(patch: Partial<McpConnectionPolicy>) {
|
||||
if (props.disabled || !selectedConnectionId.value) return;
|
||||
const next = { ...policyFor(selectedConnectionId.value), ...patch };
|
||||
const existing = policyFor(selectedConnectionId.value);
|
||||
const migration = existing.executionModePolicyVersion === 1 ? {} : { ...promoteLegacyConnectionDefault(existing), executionModePolicyVersion: 1 as const };
|
||||
const next = { ...existing, ...migration, ...patch };
|
||||
const policies = props.connectionPolicies.filter((policy) => policy.connectionId !== selectedConnectionId.value);
|
||||
emit("update:connectionPolicies", [...policies, next]);
|
||||
}
|
||||
|
||||
function setScope(scope: DatabaseScope) {
|
||||
updateSelectedPolicy({ databaseScope: scope, allowedDatabases: scope === "selected" ? selectedDatabases.value : [] });
|
||||
updateSelectedPolicy({
|
||||
databaseScope: scope,
|
||||
allowedDatabases: scope === "selected" ? selectedDatabases.value : [],
|
||||
databasePolicies: scope === "selected" ? selectedDatabasePolicies.value.filter((policy) => selectedDatabases.value.includes(policy.databaseName)) : [],
|
||||
});
|
||||
}
|
||||
|
||||
function setConnectionPage(page: number) {
|
||||
@@ -111,7 +123,77 @@ function selectConnection(connectionId: string) {
|
||||
|
||||
function toggleDatabase(database: string, checked: boolean) {
|
||||
const databases = checked ? [...new Set([...selectedDatabases.value, database])] : selectedDatabases.value.filter((item) => item !== database);
|
||||
updateSelectedPolicy({ databaseScope: "selected", allowedDatabases: databases });
|
||||
updateSelectedPolicy({
|
||||
databaseScope: "selected",
|
||||
allowedDatabases: databases,
|
||||
databasePolicies: selectedDatabasePolicies.value.filter((policy) => databases.includes(policy.databaseName)),
|
||||
});
|
||||
}
|
||||
|
||||
function databaseExecutionMode(database: string): DatabaseExecutionMode {
|
||||
const policy = selectedDatabasePolicies.value.find((item) => item.databaseName === database);
|
||||
if (!policy) return "inherit";
|
||||
if (policy.readOnly) return "read_only";
|
||||
return policy.allowDangerousSql ? "high_risk_write" : "safe_write";
|
||||
}
|
||||
|
||||
function connectionExecutionMode(): DatabaseExecutionMode {
|
||||
const policy = selectedPolicy.value;
|
||||
if (!policy || !policy.executionModeConfigured) return "inherit";
|
||||
if (policy.readOnly) return "read_only";
|
||||
return policy.allowDangerousSql ? "high_risk_write" : "safe_write";
|
||||
}
|
||||
|
||||
function effectiveDatabaseExecutionMode(database: string): ExecutionMode {
|
||||
const databaseMode = databaseExecutionMode(database);
|
||||
if (databaseMode !== "inherit") return databaseMode;
|
||||
const connectionMode = connectionExecutionMode();
|
||||
return connectionMode === "inherit" ? props.globalExecutionMode : connectionMode;
|
||||
}
|
||||
|
||||
function executionModeLabel(mode: ExecutionMode): string {
|
||||
return t(mode === "read_only" ? "settings.mcpExecutionModeReadOnly" : mode === "safe_write" ? "settings.mcpExecutionModeSafeWrite" : "settings.mcpExecutionModeHighRiskWrite");
|
||||
}
|
||||
|
||||
function executionModeRank(mode: DatabaseExecutionMode): number {
|
||||
return mode === "read_only" ? 0 : mode === "safe_write" ? 1 : mode === "high_risk_write" ? 2 : 3;
|
||||
}
|
||||
|
||||
function promoteLegacyConnectionDefault(policy: McpConnectionPolicy): Pick<McpConnectionPolicy, "readOnly" | "allowDangerousSql" | "executionModeConfigured" | "databasePolicies"> {
|
||||
if (policy.executionModePolicyVersion === 1) {
|
||||
return { readOnly: policy.readOnly, allowDangerousSql: policy.allowDangerousSql, executionModeConfigured: policy.executionModeConfigured, databasePolicies: policy.databasePolicies };
|
||||
}
|
||||
const legacyMode = connectionExecutionMode();
|
||||
const effectiveMode = executionModeRank(legacyMode) < executionModeRank(props.globalExecutionMode) ? legacyMode : props.globalExecutionMode;
|
||||
const databasePolicies = policy.databasePolicies.map((databasePolicy) => {
|
||||
const databaseMode = databasePolicy.readOnly ? "read_only" : databasePolicy.allowDangerousSql ? "high_risk_write" : "safe_write";
|
||||
const effectiveDatabaseMode = executionModeRank(databaseMode) < executionModeRank(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 setDatabaseExecutionMode(database: string, mode: DatabaseExecutionMode) {
|
||||
const existing = policyFor(selectedConnectionId.value);
|
||||
const migrated = promoteLegacyConnectionDefault(existing);
|
||||
const policies = migrated.databasePolicies.filter((policy) => policy.databaseName !== database);
|
||||
if (mode !== "inherit") {
|
||||
policies.push({
|
||||
databaseName: database,
|
||||
readOnly: mode === "read_only",
|
||||
allowDangerousSql: mode === "high_risk_write",
|
||||
});
|
||||
}
|
||||
updateSelectedPolicy({ ...promoteLegacyConnectionDefault(existing), databasePolicies: policies, executionModePolicyVersion: 1 });
|
||||
}
|
||||
|
||||
function addManualDatabase() {
|
||||
@@ -185,12 +267,21 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<button v-for="connection in pagedScopedConnections" :key="connection.id" type="button" class="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-muted" :class="selectedConnectionId === connection.id ? 'bg-muted' : ''" @click="selectConnection(connection.id)">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<button
|
||||
v-for="connection in pagedScopedConnections"
|
||||
:key="connection.id"
|
||||
type="button"
|
||||
class="w-full cursor-pointer rounded-md px-2 py-2 text-left transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
:class="selectedConnectionId === connection.id ? 'bg-muted' : ''"
|
||||
:aria-current="selectedConnectionId === connection.id ? 'true' : undefined"
|
||||
@pointerdown="selectConnection(connection.id)"
|
||||
@click="selectConnection(connection.id)"
|
||||
>
|
||||
<div class="pointer-events-none flex items-center justify-between gap-2">
|
||||
<span class="min-w-0 truncate text-sm font-medium">{{ connection.name }}</span>
|
||||
<Badge variant="outline" class="shrink-0 text-[10px] font-normal">{{ scopeSummary(connection) }}</Badge>
|
||||
</div>
|
||||
<p class="mt-0.5 truncate font-mono text-[10px] text-muted-foreground">{{ connection.db_type }} · {{ connection.host || connection.database || connection.id }}</p>
|
||||
<p class="pointer-events-none mt-0.5 truncate font-mono text-[10px] text-muted-foreground">{{ connection.db_type }} · {{ connection.host || connection.database || connection.id }}</p>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="connectionPageCount > 1" class="mt-2 flex items-center justify-center gap-2 border-t pt-2 text-xs text-muted-foreground">
|
||||
@@ -246,6 +337,16 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
|
||||
<label v-for="database in pagedDatabases" :key="database" class="flex cursor-pointer items-center gap-2 border-b px-3 py-2 text-xs last:border-b-0 hover:bg-muted/50">
|
||||
<input type="checkbox" :checked="selectedDatabases.includes(database)" :disabled="disabled || busy" @change="toggleDatabase(database, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono">{{ database }}</span>
|
||||
<div v-if="selectedDatabases.includes(database)" class="flex shrink-0 items-center gap-1.5">
|
||||
<span class="text-[11px] text-muted-foreground">{{ t("settings.mcpDatabasePolicySetting") }}</span>
|
||||
<select :value="databaseExecutionMode(database)" :disabled="disabled || busy" class="h-7 max-w-32 rounded border bg-background px-1.5 text-[11px]" @click.stop @change="setDatabaseExecutionMode(database, ($event.target as HTMLSelectElement).value as DatabaseExecutionMode)">
|
||||
<option value="inherit">{{ t("settings.mcpDatabasePolicyInherit") }}</option>
|
||||
<option value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</option>
|
||||
<option value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</option>
|
||||
<option value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</option>
|
||||
</select>
|
||||
<Badge variant="secondary" class="text-[10px] font-normal">{{ t("settings.mcpDatabasePolicyEffective", { mode: executionModeLabel(effectiveDatabaseExecutionMode(database)) }) }}</Badge>
|
||||
</div>
|
||||
<Check v-if="selectedDatabases.includes(database)" class="h-3.5 w-3.5 text-green-600" />
|
||||
</label>
|
||||
<div v-if="databasePageCount > 1" class="flex items-center justify-center gap-2 border-t px-3 py-2 text-xs text-muted-foreground">
|
||||
|
||||
Reference in new issue
Block a user