feat(mcp): add resource group scopes with permission inheritance

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-07 02:18:28 +08:00
1 parent 2eaf7f9914
commit 691ecd084b
31 files changed
+2398 -1047

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -7,13 +7,13 @@ type StepId = "capabilities" | "connections" | "databases" | "overrides";
const { t } = useI18n();
const steps = computed<Array<{ id: StepId; title: string; description: string }>>(() => [
{ id: "overrides", title: t("settings.mcpAuthStepOverridesTitle"), description: t("settings.mcpAuthStepOverridesDescription") },
{ id: "connections", title: t("settings.mcpAuthStepConnectionsTitle"), description: t("settings.mcpAuthStepConnectionsDescription") },
{ id: "databases", title: t("settings.mcpAuthStepDatabasesTitle"), description: t("settings.mcpAuthStepDatabasesDescription") },
{ id: "overrides", title: t("settings.mcpAuthStepOverridesTitle"), description: t("settings.mcpAuthStepOverridesDescription") },
{ id: "capabilities", title: t("settings.mcpAuthStepCapabilitiesTitle"), description: t("settings.mcpAuthStepCapabilitiesDescription") },
]);
const currentStep = ref<StepId>("connections");
const currentStep = ref<StepId>("overrides");
const currentIndex = computed(() => steps.value.findIndex((step) => step.id === currentStep.value));
function scrollableAncestor(target: EventTarget | null): HTMLElement | null {
@@ -56,12 +56,12 @@ function nextStep(event: Event) {
<p class="text-xs text-muted-foreground">{{ t("settings.mcpAuthDescription") }}</p>
</header>
<nav class="grid gap-1 rounded-md border bg-muted/50 p-1 sm:grid-cols-4" :aria-label="t('settings.mcpAuthStepsLabel')">
<nav class="flex items-stretch gap-1 overflow-x-auto rounded-md border bg-muted/50 p-1 sm:grid sm:grid-cols-4" :aria-label="t('settings.mcpAuthStepsLabel')">
<button
v-for="(step, index) in steps"
:key="step.id"
type="button"
class="group flex min-w-0 items-center gap-2 rounded px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
class="group flex min-h-16 min-w-48 flex-none items-start gap-2 rounded px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 sm:min-w-0"
:class="currentStep === step.id ? 'bg-background shadow-sm' : 'text-muted-foreground hover:bg-background/70'"
:aria-current="currentStep === step.id ? 'step' : undefined"
@click="selectStep(step.id, $event)"
@@ -71,16 +71,16 @@ function nextStep(event: Event) {
:class="currentStep === step.id ? 'border-primary bg-primary text-primary-foreground' : index < currentIndex ? 'border-primary/40 bg-primary/10 text-primary' : 'bg-background text-muted-foreground'"
>{{ index + 1 }}</span
>
<span class="min-w-0">
<span class="block truncate text-xs font-medium">{{ step.title }}</span>
<span class="hidden truncate text-[10px] text-muted-foreground lg:block">{{ step.description }}</span>
<span class="min-w-0 flex-1">
<span class="block whitespace-normal text-xs font-medium leading-5">{{ step.title }}</span>
<span class="mt-0.5 block whitespace-normal text-[10px] leading-4 text-muted-foreground">{{ step.description }}</span>
</span>
</button>
</nav>
<div :class="currentStep === 'connections' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'connections'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 1, title: t('settings.mcpAuthStepConnectionsTitle') })"><slot name="connections" /></div>
<div :class="currentStep === 'databases' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'databases'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 2, title: t('settings.mcpAuthStepDatabasesTitle') })"><slot name="databases" /></div>
<div :class="currentStep === 'overrides' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'overrides'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 3, title: t('settings.mcpAuthStepOverridesTitle') })"><slot name="overrides" /></div>
<div :class="currentStep === 'overrides' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'overrides'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 1, title: t('settings.mcpAuthStepOverridesTitle') })"><slot name="overrides" /></div>
<div :class="currentStep === 'connections' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'connections'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 2, title: t('settings.mcpAuthStepConnectionsTitle') })"><slot name="connections" /></div>
<div :class="currentStep === 'databases' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'databases'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 3, title: t('settings.mcpAuthStepDatabasesTitle') })"><slot name="databases" /></div>
<div :class="currentStep === 'capabilities' ? 'block' : 'hidden'" :aria-hidden="currentStep !== 'capabilities'" role="region" :aria-label="t('settings.mcpAuthStepRegionLabel', { step: 4, title: t('settings.mcpAuthStepCapabilitiesTitle') })"><slot name="capabilities" /></div>
<footer class="flex items-center justify-between gap-3 border-t pt-3">
@@ -10,19 +10,17 @@ 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">;
type ExecutionMode = "read_only" | "safe_write" | "high_risk_write";
const { t } = useI18n();
const PAGE_SIZE_OPTIONS = [6, 10, 20] as const;
const DATABASE_SCOPES: DatabaseScope[] = ["all", "selected", "none"];
const props = withDefaults(
defineProps<{
connections: readonly ConnectionConfig[];
allowedConnectionIds: readonly string[] | null;
connectionPolicies: readonly McpConnectionPolicy[];
globalExecutionMode: ExecutionMode;
disabled?: boolean;
busy?: boolean;
}>(),
@@ -31,6 +29,7 @@ const props = withDefaults(
const emit = defineEmits<{
"update:connectionPolicies": [value: McpConnectionPolicy[]];
"set:database-policy": [connectionId: string, databaseName: string, mode: ExecutionMode | "inherit"];
}>();
const selectedConnectionId = ref("");
@@ -92,13 +91,13 @@ function policyFor(connectionId: string): McpConnectionPolicy {
function updateSelectedPolicy(patch: Partial<McpConnectionPolicy>) {
if (props.disabled || !selectedConnectionId.value) return;
const existing = policyFor(selectedConnectionId.value);
const migration = existing.executionModePolicyVersion === 1 ? {} : { ...promoteLegacyConnectionDefault(existing), executionModePolicyVersion: 1 as const };
const next = { ...existing, ...migration, ...patch };
const next = { ...existing, ...patch };
const policies = props.connectionPolicies.filter((policy) => policy.connectionId !== selectedConnectionId.value);
emit("update:connectionPolicies", [...policies, next]);
}
function setScope(scope: DatabaseScope) {
if (scope !== "selected" && selectedDatabasePolicies.value.length > 0 && !window.confirm(t("settings.mcpDatabaseScopeClearsPermissionExceptions"))) return;
updateSelectedPolicy({
databaseScope: scope,
allowedDatabases: scope === "selected" ? selectedDatabases.value : [],
@@ -106,6 +105,17 @@ function setScope(scope: DatabaseScope) {
});
}
function onScopeKeydown(event: KeyboardEvent, scope: DatabaseScope) {
if (props.disabled || props.busy || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
event.preventDefault();
const currentIndex = DATABASE_SCOPES.indexOf(scope);
const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? DATABASE_SCOPES.length - 1 : event.key === "ArrowLeft" || event.key === "ArrowUp" ? (currentIndex - 1 + DATABASE_SCOPES.length) % DATABASE_SCOPES.length : (currentIndex + 1) % DATABASE_SCOPES.length;
const nextScope = DATABASE_SCOPES[nextIndex];
const group = event.currentTarget instanceof HTMLElement ? event.currentTarget.closest<HTMLElement>('[role="radiogroup"]') : null;
group?.querySelector<HTMLElement>(`[data-database-scope="${nextScope}"]`)?.focus();
setScope(nextScope);
}
function setConnectionPage(page: number) {
connectionPage.value = Math.min(Math.max(1, page), connectionPageCount.value);
}
@@ -122,6 +132,7 @@ function selectConnection(connectionId: string) {
}
function toggleDatabase(database: string, checked: boolean) {
if (!checked && selectedDatabasePolicies.value.some((policy) => policy.databaseName === database) && !window.confirm(t("settings.mcpDatabaseScopeClearsPermissionExceptions"))) return;
const databases = checked ? [...new Set([...selectedDatabases.value, database])] : selectedDatabases.value.filter((item) => item !== database);
updateSelectedPolicy({
databaseScope: "selected",
@@ -130,72 +141,13 @@ function toggleDatabase(database: string, checked: boolean) {
});
}
function databaseExecutionMode(database: string): DatabaseExecutionMode {
function databasePolicyMode(database: string): ExecutionMode | "inherit" {
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() {
const database = manualDatabase.value.trim();
if (!database) return;
@@ -295,9 +247,9 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
<div class="flex flex-wrap items-start justify-between gap-2">
<div>
<p class="text-sm font-medium">{{ selectedConnection.name }}</p>
<p class="text-xs text-muted-foreground">{{ t("settings.mcpDatabaseScopePermissionNote") }}</p>
<p class="text-xs text-muted-foreground">{{ t("settings.mcpDatabaseScopeDescription") }}</p>
</div>
<Button type="button" variant="outline" size="sm" :disabled="disabled || Boolean(loadingConnectionId)" @click="loadConnectionDatabases">
<Button v-if="selectedScope !== 'all'" type="button" variant="outline" size="sm" :disabled="disabled || Boolean(loadingConnectionId)" @click="loadConnectionDatabases">
<Loader2 v-if="loadingConnectionId === selectedConnection.id" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
<RefreshCw v-else class="mr-1.5 h-3.5 w-3.5" />
{{ t("settings.mcpDatabaseLoadButton") }}
@@ -305,19 +257,55 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
</div>
<div class="grid gap-2 sm:grid-cols-3" role="radiogroup" :aria-label="t('settings.mcpDatabaseScopeAriaLabel')">
<Button type="button" variant="outline" class="h-auto justify-start px-3 py-2 text-left" :class="selectedScope === 'all' ? 'dbx-choice-selected' : ''" :disabled="disabled || busy" @click="setScope('all')">
<Button
type="button"
role="radio"
data-database-scope="all"
variant="outline"
class="h-auto justify-start px-3 py-2 text-left"
:class="selectedScope === 'all' ? 'dbx-choice-selected' : ''"
:aria-checked="selectedScope === 'all'"
:tabindex="selectedScope === 'all' ? 0 : -1"
:disabled="disabled || busy"
@click="setScope('all')"
@keydown="onScopeKeydown($event, 'all')"
>
<span
><span class="block text-sm">{{ t("settings.mcpDatabaseScopeAllTitle") }}</span
><span class="block text-[11px] font-normal text-muted-foreground">{{ t("settings.mcpDatabaseScopeAllDescription") }}</span></span
>
</Button>
<Button type="button" variant="outline" class="h-auto justify-start px-3 py-2 text-left" :class="selectedScope === 'selected' ? 'dbx-choice-selected' : ''" :disabled="disabled || busy" @click="setScope('selected')">
<Button
type="button"
role="radio"
data-database-scope="selected"
variant="outline"
class="h-auto justify-start px-3 py-2 text-left"
:class="selectedScope === 'selected' ? 'dbx-choice-selected' : ''"
:aria-checked="selectedScope === 'selected'"
:tabindex="selectedScope === 'selected' ? 0 : -1"
:disabled="disabled || busy"
@click="setScope('selected')"
@keydown="onScopeKeydown($event, 'selected')"
>
<span
><span class="block text-sm">{{ t("settings.mcpDatabaseScopeSelectedTitle") }}</span
><span class="block text-[11px] font-normal text-muted-foreground">{{ t("settings.mcpDatabaseScopeSelectedDescription") }}</span></span
>
</Button>
<Button type="button" variant="outline" class="h-auto justify-start px-3 py-2 text-left" :class="selectedScope === 'none' ? 'dbx-choice-selected' : ''" :disabled="disabled || busy" @click="setScope('none')">
<Button
type="button"
role="radio"
data-database-scope="none"
variant="outline"
class="h-auto justify-start px-3 py-2 text-left"
:class="selectedScope === 'none' ? 'dbx-choice-selected' : ''"
:aria-checked="selectedScope === 'none'"
:tabindex="selectedScope === 'none' ? 0 : -1"
:disabled="disabled || busy"
@click="setScope('none')"
@keydown="onScopeKeydown($event, 'none')"
>
<span
><span class="block text-sm">{{ t("settings.mcpDatabaseScopeNoneTitle") }}</span
><span class="block text-[11px] font-normal text-muted-foreground">{{ t("settings.mcpDatabaseScopeNoneDescription") }}</span></span
@@ -337,17 +325,20 @@ 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" />
<select
v-if="selectedDatabases.includes(database)"
:value="databasePolicyMode(database)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:disabled="disabled || busy"
@click.stop
@change="$emit('set:database-policy', selectedConnectionId, database, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</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>
</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">
<Button type="button" size="icon-sm" variant="ghost" :disabled="databasePage === 1" :title="t('settings.mcpPreviousPage')" :aria-label="t('settings.mcpPreviousPage')" @click="setDatabasePage(databasePage - 1)"><ChevronLeft /></Button>
@@ -0,0 +1,354 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { ChevronDown, ChevronRight, Database, Folder, Search } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { ConnectionConfig, SidebarLayout, SidebarOrderEntry } from "@/types/database";
type ScopeMode = "all" | "custom";
type ExecutionMode = "read_only" | "safe_write" | "high_risk_write";
type ResourceNode = GroupNode | ConnectionNode;
interface GroupNode {
type: "group";
id: string;
name: string;
pathNames: string[];
pathIds: string[];
depth: number;
children: ResourceNode[];
}
interface ConnectionNode {
type: "connection";
id: string;
connection: ConnectionConfig;
pathIds: string[];
depth: number;
}
const props = withDefaults(
defineProps<{
layout: SidebarLayout;
connections: readonly ConnectionConfig[];
allowedGroupIds: readonly string[];
allowedConnectionIds: readonly string[] | null;
groupPolicies?: readonly { groupId: string; readOnly: boolean; allowDangerousSql: boolean }[];
connectionPolicies?: readonly { connectionId: string; readOnly: boolean; allowDangerousSql: boolean; executionModeConfigured?: boolean }[];
disabled?: boolean;
busy?: boolean;
}>(),
{ disabled: false, busy: false },
);
const emit = defineEmits<{
"update:scope": [value: { allowedGroupIds: string[]; allowedConnectionIds: string[] | null }];
"set:group-policy": [groupId: string, mode: ExecutionMode | "inherit"];
"set:connection-policy": [connectionId: string, mode: ExecutionMode | "inherit"];
}>();
const { t } = useI18n();
const search = ref("");
const expandedGroupIds = ref(new Set<string>());
const initializedExpansion = ref(false);
const scopeMode = computed<ScopeMode>(() => (props.allowedConnectionIds === null ? "all" : "custom"));
const connectionById = computed(() => new Map(props.connections.map((connection) => [connection.id, connection])));
const groupNameById = computed(() => new Map(props.layout.groups.map((group) => [group.id, group.name])));
function entryChildren(entry: Extract<SidebarOrderEntry, { type: "group" }>): SidebarOrderEntry[] {
return entry.children ?? entry.connectionIds?.map((id) => ({ type: "connection" as const, id })) ?? [];
}
const resourceTree = computed<ResourceNode[]>(() => {
const seenConnections = new Set<string>();
const seenGroups = new Set<string>();
const build = (entries: SidebarOrderEntry[], pathIds: string[], pathNames: string[], depth: number): ResourceNode[] =>
entries.flatMap((entry): ResourceNode[] => {
if (entry.type === "connection") {
const connection = connectionById.value.get(entry.id);
if (!connection || seenConnections.has(entry.id)) return [];
seenConnections.add(entry.id);
return [{ type: "connection", id: entry.id, connection, pathIds, depth }];
}
if (seenGroups.has(entry.id)) return [];
const name = groupNameById.value.get(entry.id);
if (!name) return [];
seenGroups.add(entry.id);
const nextPathIds = [...pathIds, entry.id];
const nextPathNames = [...pathNames, name];
return [{ type: "group", id: entry.id, name, pathIds: nextPathIds, pathNames: nextPathNames, depth, children: build(entryChildren(entry), nextPathIds, nextPathNames, depth + 1) }];
});
const nodes = build(props.layout.order, [], [], 0);
for (const connection of props.connections) {
if (!seenConnections.has(connection.id)) nodes.push({ type: "connection", id: connection.id, connection, pathIds: [], depth: 0 });
}
if (!initializedExpansion.value) {
expandedGroupIds.value = new Set();
initializedExpansion.value = true;
}
return nodes;
});
const selectedGroupIds = computed(() => new Set(props.allowedGroupIds));
const explicitConnectionIds = computed(() => new Set(props.allowedConnectionIds ?? []));
function selectedAncestor(node: ResourceNode): string | null {
const ancestors = node.type === "group" ? node.pathIds.slice(0, -1) : node.pathIds;
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
if (selectedGroupIds.value.has(ancestors[index])) return ancestors[index];
}
return null;
}
function groupLabel(groupId: string): string {
const group = allGroups.value.get(groupId);
return group?.pathNames.join(" / ") ?? groupId;
}
const allGroups = computed(() => {
const result = new Map<string, GroupNode>();
const visit = (nodes: ResourceNode[]) => {
for (const node of nodes) {
if (node.type !== "group") continue;
result.set(node.id, node);
visit(node.children);
}
};
visit(resourceTree.value);
return result;
});
function descendants(group: GroupNode): { groupIds: string[]; connectionIds: string[] } {
const groupIds: string[] = [];
const connectionIds: string[] = [];
const visit = (nodes: ResourceNode[]) => {
for (const node of nodes) {
if (node.type === "connection") connectionIds.push(node.id);
else {
groupIds.push(node.id);
visit(node.children);
}
}
};
visit(group.children);
return { groupIds, connectionIds };
}
const effectiveConnectionIds = computed(() => {
if (props.allowedConnectionIds === null) return new Set(props.connections.map((connection) => connection.id));
const result = new Set(props.allowedConnectionIds);
for (const groupId of props.allowedGroupIds) {
const group = allGroups.value.get(groupId);
if (!group) continue;
for (const connectionId of descendants(group).connectionIds) result.add(connectionId);
}
return result;
});
const groupCoveredConnectionIds = computed(() => {
const result = new Set<string>();
for (const groupId of props.allowedGroupIds) {
const group = allGroups.value.get(groupId);
if (!group) continue;
for (const connectionId of descendants(group).connectionIds) result.add(connectionId);
}
return result;
});
const directConnectionCount = computed(() => props.connections.filter((connection) => explicitConnectionIds.value.has(connection.id) && !groupCoveredConnectionIds.value.has(connection.id)).length);
const directGroupCount = computed(
() =>
props.allowedGroupIds.filter((groupId) => {
const group = allGroups.value.get(groupId);
return group && !selectedAncestor(group);
}).length,
);
const summary = computed(() =>
scopeMode.value === "all"
? t("settings.mcpResourceScopeAllSummary", { count: props.connections.length })
: t("settings.mcpResourceScopeCustomSummary", {
effective: props.connections.filter((connection) => effectiveConnectionIds.value.has(connection.id)).length,
groups: directGroupCount.value,
connections: directConnectionCount.value,
}),
);
function searchableText(node: ResourceNode): string {
if (node.type === "group") return node.pathNames.join(" ").toLocaleLowerCase();
const connection = node.connection;
return [connection.name, connection.db_type, connection.host, connection.port, connection.database, connection.id].filter(Boolean).join(" ").toLocaleLowerCase();
}
function subtreeMatches(node: ResourceNode, query: string): boolean {
if (searchableText(node).includes(query)) return true;
return node.type === "group" && node.children.some((child) => subtreeMatches(child, query));
}
const visibleRows = computed(() => {
const rows: ResourceNode[] = [];
const query = search.value.trim().toLocaleLowerCase();
const visit = (nodes: ResourceNode[], includeAll = false) => {
for (const node of nodes) {
const ownMatch = !query || searchableText(node).includes(query);
if (query && !includeAll && !subtreeMatches(node, query)) continue;
rows.push(node);
if (node.type === "group" && (query || expandedGroupIds.value.has(node.id))) visit(node.children, includeAll || ownMatch);
}
};
visit(resourceTree.value);
return rows;
});
function setScopeMode(mode: ScopeMode) {
if (props.disabled || mode === scopeMode.value) return;
if (mode === "all" && (props.allowedGroupIds.length > 0 || (props.allowedConnectionIds?.length ?? 0) > 0) && !window.confirm(t("settings.mcpResourceScopeAllConfirm"))) return;
emit("update:scope", mode === "all" ? { allowedGroupIds: [], allowedConnectionIds: null } : { allowedGroupIds: [], allowedConnectionIds: [] });
}
function groupHasSelectedDescendant(group: GroupNode): boolean {
const childIds = descendants(group);
return childIds.groupIds.some((id) => selectedGroupIds.value.has(id)) || childIds.connectionIds.some((id) => explicitConnectionIds.value.has(id));
}
function toggleExpanded(groupId: string) {
const next = new Set(expandedGroupIds.value);
if (next.has(groupId)) next.delete(groupId);
else next.add(groupId);
expandedGroupIds.value = next;
}
function setGroupAllowed(group: GroupNode, allowed: boolean) {
if (props.disabled || selectedAncestor(group)) return;
const childIds = descendants(group);
const nextGroups = props.allowedGroupIds.filter((id) => id !== group.id && (!allowed || !childIds.groupIds.includes(id)));
const nextConnections = (props.allowedConnectionIds ?? []).filter((id) => !allowed || !childIds.connectionIds.includes(id));
if (allowed) nextGroups.push(group.id);
emit("update:scope", { allowedGroupIds: nextGroups, allowedConnectionIds: nextConnections });
}
function setConnectionAllowed(connectionId: string, allowed: boolean) {
if (props.disabled) return;
const next = new Set(props.allowedConnectionIds ?? []);
if (allowed) next.add(connectionId);
else next.delete(connectionId);
emit("update:scope", { allowedGroupIds: [...props.allowedGroupIds], allowedConnectionIds: [...next] });
}
function policyMode(policy: { readOnly: boolean; allowDangerousSql: boolean; executionModeConfigured?: boolean } | undefined): ExecutionMode | "inherit" {
if (!policy || policy.executionModeConfigured === false) return "inherit";
if (policy.readOnly) return "read_only";
return policy.allowDangerousSql ? "high_risk_write" : "safe_write";
}
function groupPolicyMode(groupId: string): ExecutionMode | "inherit" {
return policyMode(props.groupPolicies?.find((policy) => policy.groupId === groupId));
}
function connectionPolicyMode(connectionId: string): ExecutionMode | "inherit" {
return policyMode(props.connectionPolicies?.find((policy) => policy.connectionId === connectionId));
}
</script>
<template>
<section class="overflow-hidden rounded-md border bg-background" :aria-busy="busy">
<div class="flex flex-wrap items-start justify-between gap-3 p-3">
<div class="min-w-0 space-y-1">
<p class="text-sm font-medium">{{ t("settings.mcpResourceScopeTitle") }}</p>
<p class="text-xs text-muted-foreground">{{ t("settings.mcpResourceScopeDescription") }}</p>
</div>
<Badge variant="outline" class="max-w-full rounded-md font-normal">{{ summary }}</Badge>
</div>
<div class="grid grid-cols-2 gap-1 border-t bg-muted/40 p-1" role="radiogroup" :aria-label="t('settings.mcpResourceScopeTitle')">
<Button type="button" role="radio" variant="ghost" class="h-9" :class="scopeMode === 'all' ? 'bg-background shadow-sm' : 'text-muted-foreground'" :aria-checked="scopeMode === 'all'" :disabled="disabled" @click="setScopeMode('all')">
{{ t("settings.mcpScopeModeAll") }}
</Button>
<Button type="button" role="radio" variant="ghost" class="h-9" :class="scopeMode === 'custom' ? 'bg-background shadow-sm' : 'text-muted-foreground'" :aria-checked="scopeMode === 'custom'" :disabled="disabled" @click="setScopeMode('custom')">
{{ t("settings.mcpResourceScopeModeCustom") }}
</Button>
</div>
<template v-if="scopeMode === 'custom'">
<div class="border-t p-2">
<div class="relative">
<Search class="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input v-model="search" class="h-8 pl-8 text-xs" :placeholder="t('settings.mcpResourceScopeSearch')" :disabled="disabled" />
</div>
</div>
<div v-if="visibleRows.length" class="max-h-80 overflow-auto border-t py-1" role="tree" :aria-label="t('settings.mcpResourceScopeTreeLabel')">
<div
v-for="node in visibleRows"
:key="`${node.type}:${node.id}`"
role="treeitem"
class="flex min-h-9 items-center gap-2 px-2 text-xs hover:bg-muted/50"
:aria-level="node.depth + 1"
:aria-expanded="node.type === 'group' ? (search.trim() ? true : expandedGroupIds.has(node.id)) : undefined"
:style="{ paddingLeft: `${8 + node.depth * 18}px` }"
>
<button v-if="node.type === 'group'" type="button" class="flex size-6 shrink-0 items-center justify-center rounded hover:bg-muted" :aria-label="node.name" @click="toggleExpanded(node.id)">
<ChevronDown v-if="search.trim() || expandedGroupIds.has(node.id)" class="size-3.5" />
<ChevronRight v-else class="size-3.5" />
</button>
<span v-else class="size-6 shrink-0" />
<input
v-if="node.type === 'group'"
type="checkbox"
:checked="selectedGroupIds.has(node.id) || Boolean(selectedAncestor(node))"
:indeterminate="!selectedGroupIds.has(node.id) && !selectedAncestor(node) && groupHasSelectedDescendant(node)"
:disabled="disabled || Boolean(selectedAncestor(node))"
:aria-checked="!selectedGroupIds.has(node.id) && !selectedAncestor(node) && groupHasSelectedDescendant(node) ? 'mixed' : selectedGroupIds.has(node.id) || Boolean(selectedAncestor(node))"
:aria-label="node.pathNames.join(' / ')"
@change="setGroupAllowed(node, ($event.target as HTMLInputElement).checked)"
/>
<input v-else type="checkbox" :checked="explicitConnectionIds.has(node.id) || Boolean(selectedAncestor(node))" :disabled="disabled || Boolean(selectedAncestor(node))" :aria-label="node.connection.name" @change="setConnectionAllowed(node.id, ($event.target as HTMLInputElement).checked)" />
<Folder v-if="node.type === 'group'" class="size-4 shrink-0 text-muted-foreground" />
<Database v-else class="size-4 shrink-0 text-muted-foreground" />
<div class="min-w-0 flex-1">
<p class="truncate font-medium">{{ node.type === "group" ? node.name : node.connection.name }}</p>
<p v-if="node.type === 'connection'" class="truncate font-mono text-[10px] text-muted-foreground">{{ node.connection.db_type }} · {{ node.connection.host || node.connection.database || node.id }}</p>
</div>
<select
v-if="node.type === 'group' && selectedGroupIds.has(node.id)"
:value="groupPolicyMode(node.id)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:disabled="disabled || busy"
@click.stop
@change="emit('set:group-policy', node.id, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</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>
<select
v-if="node.type === 'connection' && (explicitConnectionIds.has(node.id) || Boolean(selectedAncestor(node)))"
:value="connectionPolicyMode(node.id)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:disabled="disabled || busy"
@click.stop
@change="emit('set:connection-policy', node.id, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ selectedAncestor(node) ? t("settings.mcpGroupPolicyInherit") : t("settings.mcpConnectionPolicyInherit") }}</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 v-if="selectedAncestor(node)" variant="secondary" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopeInherited", { group: groupLabel(selectedAncestor(node)!) }) }}</Badge>
<Badge v-else-if="node.type === 'group' && selectedGroupIds.has(node.id)" variant="outline" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopeDynamic") }}</Badge>
<Badge v-else-if="node.type === 'group' && groupHasSelectedDescendant(node)" variant="secondary" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopePartial") }}</Badge>
<Badge v-else-if="node.type === 'connection' && explicitConnectionIds.has(node.id)" variant="outline" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopeDirect") }}</Badge>
</div>
</div>
<p v-else class="border-t px-3 py-8 text-center text-xs text-muted-foreground">{{ t("settings.mcpResourceScopeEmpty") }}</p>
<p class="border-t px-3 py-2 text-[11px] text-muted-foreground">{{ t("settings.mcpResourceScopeHint") }}</p>
</template>
</section>
</template>
+49 -11
View File
@@ -7270,20 +7270,37 @@ export default {
mcpRefresh: "Check again",
mcpGuide: "MCP guide",
mcpAuthTitle: "MCP authorization",
mcpAuthDescription: "Complete the authorization in order: choose connections first, then limit databases and connection exceptions, and finally set the available tools and the global execution level.",
mcpAuthDescription: "Complete authorization in order: set the global default, choose groups and connections, limit databases, then review the final policy.",
mcpAuthStepsLabel: "MCP authorization steps",
mcpAuthStepConnectionsTitle: "Connection scope",
mcpAuthStepConnectionsDescription: "Decides which connections MCP can see",
mcpAuthStepConnectionsTitle: "Groups and connections",
mcpAuthStepConnectionsDescription: "Choose what MCP can see and set connection permissions",
mcpAuthStepDatabasesTitle: "Database scope",
mcpAuthStepDatabasesDescription: "Limit the databases each connection can access",
mcpAuthStepOverridesTitle: "Connection defaults",
mcpAuthStepOverridesDescription: "Set the default execution permission for each connection",
mcpAuthStepCapabilitiesTitle: "Capabilities and execution",
mcpAuthStepCapabilitiesDescription: "Choose MCP tools and the global default operation level",
mcpAuthStepDatabasesDescription: "Limit each connection's databases and set database permissions",
mcpAuthStepOverridesTitle: "Default permissions",
mcpAuthStepOverridesDescription: "Set the global default for all later scopes",
mcpAuthStepCapabilitiesTitle: "Summary and review",
mcpAuthStepCapabilitiesDescription: "Review effective permissions, choose tools, and confirm",
mcpAuthStepRegionLabel: "Step {step}: {title}",
mcpAuthPreviousStep: "Previous",
mcpAuthNextStep: "Next",
mcpAuthStepProgress: "Step {current} of {total}",
mcpResourceScopeTitle: "Connections visible to MCP",
mcpResourceScopeDescription: "Select groups for dynamic coverage and individual connections for precise exceptions.",
mcpResourceScopeModeCustom: "Custom scope",
mcpResourceScopeAllSummary: "All {count} connections",
mcpResourceScopeCustomSummary: "{effective} effective · {groups} group rules · {connections} direct rules",
mcpResourceScopeSearch: "Search groups or connections",
mcpResourceScopeTreeLabel: "MCP connection resource tree",
mcpResourceScopeInherited: "From {group}",
mcpResourceScopeDynamic: "Dynamic",
mcpResourceScopeDirect: "Direct",
mcpResourceScopePartial: "Partially allowed",
mcpResourceScopeEmpty: "No matching groups or connections.",
mcpResourceScopeHint: "A selected group includes its current and future descendant connections. Direct selections are independent exceptions.",
mcpResourceScopeAllConfirm: "Switching to all connections removes the current custom group and connection rules. Continue?",
mcpPermissionGlobalDefaultHint: "Group, connection, and database permissions override this default, but never expand connection visibility.",
mcpDatabasePolicyHighRiskConfirm: "Allow high-risk writes for this database? Connection read-only and production protections still apply.",
mcpDatabaseScopeClearsPermissionExceptions: "Changing this database scope will remove its database permission exceptions. Continue?",
mcpPerPage: "Per page",
mcpPreviousPage: "Previous page",
mcpNextPage: "Next page",
@@ -7295,7 +7312,7 @@ export default {
mcpDatabaseScopeSummaryNone: "No database access",
mcpDatabaseScopeSummarySelected: "{count} selected",
mcpDatabaseScopePermissionNote: "The selected scope does not change the connection's existing database permissions in DBX.",
mcpDatabaseLoadButton: "Load from connection",
mcpDatabaseLoadButton: "Load resource list",
mcpDatabaseScopeAriaLabel: "Database access scope",
mcpDatabaseScopeAllTitle: "All databases",
mcpDatabaseScopeAllDescription: "Keep the current access scope",
@@ -7308,19 +7325,40 @@ export default {
mcpDatabasePolicySetting: "Database priority setting",
mcpDatabasePolicyEffective: "Effective: {mode}",
mcpPermissionPreviewTitle: "Permission preview",
mcpPermissionPreviewDescription: "Permissions fall back by scope: database setting first, then connection default, then global default.",
mcpPermissionPreviewDescription: "Permission precedence: database, connection, nearest group, ancestor group, then global default.",
mcpPermissionPreviewConnection: "Connection",
mcpPermissionPreviewDatabase: "Database",
mcpPermissionPreviewConnectionDefault: "Connection default",
mcpPermissionPreviewGroupDefault: "Group default",
mcpGroupScopeTitle: "Connection groups",
mcpGroupScopeDescription: "Selecting a group dynamically exposes all of its current and future descendant connections. Other connections can still be added explicitly.",
mcpGroupScopeSelected: "{count} groups selected",
mcpGroupScopeSearch: "Search group paths",
mcpGroupScopeNoResults: "No matching groups found.",
mcpGroupScopeEmpty: "There are no connection groups to configure.",
mcpGroupPolicyTitle: "Group default permissions",
mcpGroupPolicyDescription: "A child group overrides its parent; connection and database settings can override the group default.",
mcpGroupPolicyInherit: "Inherit parent or global",
mcpGroupPolicyHighRiskConfirm: "Allow high-risk writes for this group? Connection read-only and production protections still apply.",
mcpPermissionPreviewDatabaseOverride: "Database priority setting",
mcpPermissionPreviewEffective: "Effective",
mcpPermissionPreviewResource: "Resource",
mcpPermissionPreviewSource: "Source",
mcpPermissionSourceDatabase: "Database exception",
mcpPermissionSourceConnection: "Connection exception",
mcpPermissionSourceGroup: "Group: {group}",
mcpPermissionSourceGlobal: "Global default",
mcpPermissionPreviewEmpty: "Allow at least one connection and configure its database scope first.",
mcpPermissionPreviewSearchPlaceholder: "Search connections or databases",
mcpPermissionPreviewSearchNoResults: "No matching connections or databases found.",
mcpDatabaseSearchPlaceholder: "Filter loaded databases",
mcpDatabaseManualPlaceholder: "Add a database name manually",
mcpDatabaseLoadFailed: "Failed to load databases: {error}. You can add the exact database names manually.",
mcpDatabaseEmptyHint: "Click 'Load from connection' to fetch the database list, or add allowed databases manually.",
mcpDatabaseEmptyHint: "Click 'Load resource list' to fetch the database list, or add allowed databases manually.",
mcpManagementAccess: "Access methods",
mcpManagementPermissions: "Permission management",
mcpTransportLabel: "Connection type",
mcpRuntimeSettings: "Runtime settings",
mcpTransportLocalStdio: "Local stdio",
mcpTransportHttpService: "HTTP service",
mcpHttpWebServiceTitle: "Web Streamable HTTP service",
+49 -11
View File
@@ -6815,20 +6815,43 @@ export default withEnglishFallback({
mcpRefresh: "Comprobar de nuevo",
mcpGuide: "Guía MCP",
mcpAuthTitle: "Autorización de MCP",
mcpAuthDescription: "Completa la autorización en orden: elige primero las conexiones, luego limita las bases de datos y las excepciones por conexión y, por último, define las herramientas disponibles y el nivel de ejecución global.",
mcpAuthDescription: "Completa la autorización en orden: define el valor global, elige grupos y conexiones, limita las bases de datos y revisa la política final.",
mcpAuthStepsLabel: "Pasos de autorización de MCP",
mcpAuthStepConnectionsTitle: "Alcance de conexiones",
mcpAuthStepConnectionsDescription: "Define qué conexiones puede ver MCP",
mcpAuthStepConnectionsTitle: "Grupos y conexiones",
mcpAuthStepConnectionsDescription: "Elige lo que MCP puede ver y configura los permisos",
mcpAuthStepDatabasesTitle: "Alcance de bases de datos",
mcpAuthStepDatabasesDescription: "Limita las bases de datos accesibles por conexión",
mcpAuthStepOverridesTitle: "Valores predeterminados de conexión",
mcpAuthStepOverridesDescription: "Define el permiso de ejecución predeterminado de cada conexión",
mcpAuthStepCapabilitiesTitle: "Capacidades y ejecución",
mcpAuthStepCapabilitiesDescription: "Elige las herramientas MCP y el nivel operativo global predeterminado",
mcpAuthStepDatabasesDescription: "Limita las bases de datos y configura permisos por base",
mcpAuthStepOverridesTitle: "Permisos predeterminados",
mcpAuthStepOverridesDescription: "Define el permiso global para los ámbitos posteriores",
mcpAuthStepCapabilitiesTitle: "Resumen y confirmación",
mcpAuthStepCapabilitiesDescription: "Revisa los permisos efectivos, elige herramientas y confirma",
mcpAuthStepRegionLabel: "Paso {step}: {title}",
mcpAuthPreviousStep: "Anterior",
mcpAuthNextStep: "Siguiente",
mcpAuthStepProgress: "Paso {current} de {total}",
mcpResourceScopeTitle: "Conexiones visibles para MCP",
mcpResourceScopeDescription: "Selecciona grupos para una cobertura dinámica y conexiones individuales para excepciones precisas.",
mcpResourceScopeModeCustom: "Ámbito personalizado",
mcpResourceScopeAllSummary: "Las {count} conexiones",
mcpResourceScopeCustomSummary: "{effective} efectivas · {groups} reglas de grupo · {connections} reglas directas",
mcpResourceScopeSearch: "Buscar grupos o conexiones",
mcpResourceScopeTreeLabel: "Árbol de recursos de conexiones MCP",
mcpResourceScopeInherited: "De {group}",
mcpResourceScopeDynamic: "Dinámico",
mcpResourceScopeDirect: "Directo",
mcpResourceScopePartial: "Permitido parcialmente",
mcpResourceScopeEmpty: "No hay grupos ni conexiones coincidentes.",
mcpResourceScopeHint: "Un grupo seleccionado incluye sus conexiones descendientes actuales y futuras. Las selecciones directas son excepciones independientes.",
mcpResourceScopeAllConfirm: "Al cambiar a todas las conexiones se eliminan las reglas actuales de grupos y conexiones personalizadas. ¿Continuar?",
mcpPermissionGlobalDefaultHint: "Los permisos de grupo, conexión y base de datos reemplazan este valor predeterminado, pero nunca amplían la visibilidad de las conexiones.",
mcpDatabasePolicyHighRiskConfirm: "¿Permitir escrituras de alto riesgo para esta base de datos? Las protecciones de solo lectura y producción de la conexión siguen aplicándose.",
mcpDatabaseScopeClearsPermissionExceptions: "Al cambiar este ámbito de base de datos se eliminarán sus excepciones de permisos. ¿Continuar?",
mcpPermissionPreviewResource: "Recurso",
mcpPermissionPreviewSource: "Origen",
mcpPermissionSourceDatabase: "Excepción de base de datos",
mcpPermissionSourceConnection: "Excepción de conexión",
mcpPermissionSourceGroup: "Grupo: {group}",
mcpPermissionSourceGlobal: "Predeterminado global",
mcpPerPage: "Por página",
mcpPreviousPage: "Página anterior",
mcpNextPage: "Página siguiente",
@@ -6840,7 +6863,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "Sin acceso a bases de datos",
mcpDatabaseScopeSummarySelected: "{count} seleccionadas",
mcpDatabaseScopePermissionNote: "El alcance seleccionado no cambia los permisos de bases de datos que la conexión ya tiene en DBX.",
mcpDatabaseLoadButton: "Cargar desde la conexión",
mcpDatabaseLoadButton: "Cargar lista de recursos",
mcpDatabaseScopeAriaLabel: "Alcance de acceso a bases de datos",
mcpDatabaseScopeAllTitle: "Todas las bases de datos",
mcpDatabaseScopeAllDescription: "Mantiene el alcance de acceso actual",
@@ -6853,10 +6876,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "Configuración prioritaria de base de datos",
mcpDatabasePolicyEffective: "Efectivo: {mode}",
mcpPermissionPreviewTitle: "Vista previa de permisos",
mcpPermissionPreviewDescription: "Los permisos se resuelven por alcance: base de datos, luego conexión y finalmente valor global.",
mcpPermissionPreviewDescription: "Prioridad: base de datos, conexión, grupo más cercano, grupo superior y valor global.",
mcpPermissionPreviewConnection: "Conexión",
mcpPermissionPreviewDatabase: "Base de datos",
mcpPermissionPreviewConnectionDefault: "Predeterminado de conexión",
mcpPermissionPreviewGroupDefault: "Predeterminado del grupo",
mcpGroupScopeTitle: "Grupos de conexiones",
mcpGroupScopeDescription: "Seleccionar un grupo expone dinámicamente todas sus conexiones descendientes actuales y futuras. También puede agregar otras conexiones de forma explícita.",
mcpGroupScopeSelected: "{count} grupos seleccionados",
mcpGroupScopeSearch: "Buscar rutas de grupos",
mcpGroupScopeNoResults: "No se encontraron grupos coincidentes.",
mcpGroupScopeEmpty: "No hay grupos de conexiones para configurar.",
mcpGroupPolicyTitle: "Permisos predeterminados del grupo",
mcpGroupPolicyDescription: "Un grupo hijo reemplaza al padre; la conexión y la base de datos pueden reemplazar al grupo.",
mcpGroupPolicyInherit: "Heredar del padre o global",
mcpGroupPolicyHighRiskConfirm: "¿Permitir escrituras de alto riesgo para este grupo? Las protecciones de solo lectura y producción siguen activas.",
mcpPermissionPreviewDatabaseOverride: "Configuración prioritaria de base de datos",
mcpPermissionPreviewEffective: "Efectivo",
mcpPermissionPreviewEmpty: "Permite primero al menos una conexión y configura su alcance de bases de datos.",
@@ -6865,7 +6899,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "Filtrar bases de datos cargadas",
mcpDatabaseManualPlaceholder: "Añadir nombre de base de datos manualmente",
mcpDatabaseLoadFailed: "Error al cargar bases de datos: {error}. Puedes añadir manualmente los nombres exactos.",
mcpDatabaseEmptyHint: "Haz clic en 'Cargar desde la conexión' para obtener la lista de bases de datos o añade manualmente las permitidas.",
mcpDatabaseEmptyHint: "Haz clic en 'Cargar lista de recursos' para obtener la lista de bases de datos o añade manualmente las permitidas.",
mcpManagementAccess: "Métodos de acceso",
mcpManagementPermissions: "Gestión de permisos",
mcpTransportLabel: "Tipo de conexión",
mcpRuntimeSettings: "Ajustes de ejecución",
mcpTransportLocalStdio: "stdio local",
mcpTransportHttpService: "Servicio HTTP",
mcpHttpWebServiceTitle: "Servicio Web Streamable HTTP",
+47 -9
View File
@@ -6815,20 +6815,43 @@ export default withEnglishFallback({
mcpRefresh: "Verifica di nuovo",
mcpGuide: "Guida MCP",
mcpAuthTitle: "Autorizzazione MCP",
mcpAuthDescription: "Completa l'autorizzazione in ordine: scegli prima le connessioni, poi limita i database e le eccezioni per connessione e infine definisci gli strumenti disponibili e il livello di esecuzione globale.",
mcpAuthDescription: "Completa l'autorizzazione in ordine: imposta il valore globale, scegli gruppi e connessioni, limita i database e rivedi la policy finale.",
mcpAuthStepsLabel: "Passaggi di autorizzazione MCP",
mcpAuthStepConnectionsTitle: "Ambito connessioni",
mcpAuthStepConnectionsTitle: "Gruppi e connessioni",
mcpAuthStepConnectionsDescription: "Determina quali connessioni MCP può vedere",
mcpAuthStepDatabasesTitle: "Ambito database",
mcpAuthStepDatabasesDescription: "Limita i database accessibili per ogni connessione",
mcpAuthStepOverridesTitle: "Impostazioni predefinite della connessione",
mcpAuthStepOverridesDescription: "Imposta il permesso di esecuzione predefinito per ogni connessione",
mcpAuthStepCapabilitiesTitle: "Capacità ed esecuzione",
mcpAuthStepCapabilitiesDescription: "Scegli gli strumenti MCP e il livello operativo globale predefinito",
mcpAuthStepOverridesTitle: "Permessi predefiniti",
mcpAuthStepOverridesDescription: "Imposta il permesso globale per gli ambiti successivi",
mcpAuthStepCapabilitiesTitle: "Riepilogo e conferma",
mcpAuthStepCapabilitiesDescription: "Rivedi i permessi effettivi, scegli gli strumenti e conferma",
mcpAuthStepRegionLabel: "Passaggio {step}: {title}",
mcpAuthPreviousStep: "Indietro",
mcpAuthNextStep: "Avanti",
mcpAuthStepProgress: "Passaggio {current} di {total}",
mcpResourceScopeTitle: "Connessioni visibili a MCP",
mcpResourceScopeDescription: "Seleziona gruppi per una copertura dinamica e singole connessioni per eccezioni precise.",
mcpResourceScopeModeCustom: "Ambito personalizzato",
mcpResourceScopeAllSummary: "Tutte le {count} connessioni",
mcpResourceScopeCustomSummary: "{effective} effettive · {groups} regole di gruppo · {connections} regole dirette",
mcpResourceScopeSearch: "Cerca gruppi o connessioni",
mcpResourceScopeTreeLabel: "Albero delle risorse di connessione MCP",
mcpResourceScopeInherited: "Da {group}",
mcpResourceScopeDynamic: "Dinamico",
mcpResourceScopeDirect: "Diretto",
mcpResourceScopePartial: "Consentito parzialmente",
mcpResourceScopeEmpty: "Nessun gruppo o connessione corrispondente.",
mcpResourceScopeHint: "Un gruppo selezionato include le connessioni discendenti attuali e future. Le selezioni dirette sono eccezioni indipendenti.",
mcpResourceScopeAllConfirm: "Il passaggio a tutte le connessioni rimuove le regole correnti di gruppi e connessioni personalizzate. Continuare?",
mcpPermissionGlobalDefaultHint: "I permessi di gruppo, connessione e database sostituiscono questo valore predefinito, ma non ampliano mai la visibilità delle connessioni.",
mcpDatabasePolicyHighRiskConfirm: "Consentire scritture ad alto rischio per questo database? Le protezioni di sola lettura e produzione della connessione restano attive.",
mcpDatabaseScopeClearsPermissionExceptions: "La modifica di questo ambito database rimuoverà le relative eccezioni di autorizzazione. Continuare?",
mcpPermissionPreviewResource: "Risorsa",
mcpPermissionPreviewSource: "Origine",
mcpPermissionSourceDatabase: "Eccezione database",
mcpPermissionSourceConnection: "Eccezione connessione",
mcpPermissionSourceGroup: "Gruppo: {group}",
mcpPermissionSourceGlobal: "Predefinito globale",
mcpPerPage: "Per pagina",
mcpPreviousPage: "Pagina precedente",
mcpNextPage: "Pagina successiva",
@@ -6840,7 +6863,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "Nessun accesso ai database",
mcpDatabaseScopeSummarySelected: "{count} selezionati",
mcpDatabaseScopePermissionNote: "L'ambito selezionato non modifica le autorizzazioni database già esistenti della connessione in DBX.",
mcpDatabaseLoadButton: "Carica dalla connessione",
mcpDatabaseLoadButton: "Carica elenco risorse",
mcpDatabaseScopeAriaLabel: "Ambito di accesso ai database",
mcpDatabaseScopeAllTitle: "Tutti i database",
mcpDatabaseScopeAllDescription: "Mantiene l'ambito di accesso attuale",
@@ -6853,10 +6876,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "Impostazione prioritaria database",
mcpDatabasePolicyEffective: "Effettivo: {mode}",
mcpPermissionPreviewTitle: "Anteprima permessi",
mcpPermissionPreviewDescription: "I permessi seguono la priorità di ambito: database, poi connessione, infine valore globale.",
mcpPermissionPreviewDescription: "Priorità: database, connessione, gruppo più vicino, gruppo superiore e valore globale.",
mcpPermissionPreviewConnection: "Connessione",
mcpPermissionPreviewDatabase: "Database",
mcpPermissionPreviewConnectionDefault: "Predefinito connessione",
mcpPermissionPreviewGroupDefault: "Predefinito gruppo",
mcpGroupScopeTitle: "Gruppi di connessioni",
mcpGroupScopeDescription: "La selezione di un gruppo espone dinamicamente tutte le connessioni discendenti presenti e future. È ancora possibile aggiungere altre connessioni.",
mcpGroupScopeSelected: "{count} gruppi selezionati",
mcpGroupScopeSearch: "Cerca percorsi gruppo",
mcpGroupScopeNoResults: "Nessun gruppo corrispondente.",
mcpGroupScopeEmpty: "Non ci sono gruppi di connessioni da configurare.",
mcpGroupPolicyTitle: "Permessi predefiniti del gruppo",
mcpGroupPolicyDescription: "Un gruppo figlio sostituisce il padre; connessione e database possono sostituire il gruppo.",
mcpGroupPolicyInherit: "Eredita dal padre o globale",
mcpGroupPolicyHighRiskConfirm: "Consentire scritture ad alto rischio per questo gruppo? Le protezioni di sola lettura e produzione restano attive.",
mcpPermissionPreviewDatabaseOverride: "Impostazione prioritaria database",
mcpPermissionPreviewEffective: "Effettivo",
mcpPermissionPreviewEmpty: "Consenti prima almeno una connessione e configurane l'ambito database.",
@@ -6865,7 +6899,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "Filtra i database caricati",
mcpDatabaseManualPlaceholder: "Aggiungi manualmente un nome di database",
mcpDatabaseLoadFailed: "Caricamento dei database non riuscito: {error}. Puoi aggiungere manualmente i nomi esatti.",
mcpDatabaseEmptyHint: "Fai clic su 'Carica dalla connessione' per ottenere l'elenco dei database oppure aggiungi manualmente quelli consentiti.",
mcpDatabaseEmptyHint: "Fai clic su 'Carica elenco risorse' per ottenere l'elenco dei database oppure aggiungi manualmente quelli consentiti.",
mcpManagementAccess: "Modalità di accesso",
mcpManagementPermissions: "Gestione autorizzazioni",
mcpTransportLabel: "Tipo di connessione",
mcpRuntimeSettings: "Impostazioni di esecuzione",
mcpTransportLocalStdio: "stdio locale",
mcpTransportHttpService: "Servizio HTTP",
mcpHttpWebServiceTitle: "Servizio Web Streamable HTTP",
+47 -9
View File
@@ -6822,20 +6822,43 @@ export default withEnglishFallback({
mcpRefresh: "再確認",
mcpGuide: "MCPガイド",
mcpAuthTitle: "MCP 権限設定",
mcpAuthDescription: "順番に設定を進めてください:まず接続を選択し、次にデータベースと接続ごとの例外を制限し、最後に利用するツールとグローバル実行レベルを確定します。",
mcpAuthDescription: "順番に設定します:グローバル既定値、グループと接続、データベース範囲を設定し、最後に最終ポリシーを確認します。",
mcpAuthStepsLabel: "MCP 権限設定の手順",
mcpAuthStepConnectionsTitle: "接続範囲",
mcpAuthStepConnectionsTitle: "グループと接続",
mcpAuthStepConnectionsDescription: "MCP に公開する接続を決定します",
mcpAuthStepDatabasesTitle: "データベース範囲",
mcpAuthStepDatabasesDescription: "接続ごとにアクセス可能なデータベースを制限します",
mcpAuthStepOverridesTitle: "接続の既定権限",
mcpAuthStepOverridesDescription: "接続ごとの既定の実行権限を設定します",
mcpAuthStepCapabilitiesTitle: "機能と実行",
mcpAuthStepCapabilitiesDescription: "MCP ツールとグローバル既定の操作レベルを選択します",
mcpAuthStepOverridesTitle: "既定の権限",
mcpAuthStepOverridesDescription: "後続の範囲に適用するグローバル既定値を設定します",
mcpAuthStepCapabilitiesTitle: "概要と確認",
mcpAuthStepCapabilitiesDescription: "有効な権限を確認し、ツールを選択して確定します",
mcpAuthStepRegionLabel: "ステップ {step}:{title}",
mcpAuthPreviousStep: "前へ",
mcpAuthNextStep: "次へ",
mcpAuthStepProgress: "ステップ {current} / {total}",
mcpResourceScopeTitle: "MCP から表示する接続",
mcpResourceScopeDescription: "動的な範囲にはグループを、個別の例外には接続を選択します。",
mcpResourceScopeModeCustom: "カスタム範囲",
mcpResourceScopeAllSummary: "全 {count} 接続",
mcpResourceScopeCustomSummary: "有効 {effective} 件 · グループルール {groups} 件 · 直接ルール {connections} 件",
mcpResourceScopeSearch: "グループまたは接続を検索",
mcpResourceScopeTreeLabel: "MCP 接続リソースツリー",
mcpResourceScopeInherited: "{group} から",
mcpResourceScopeDynamic: "動的",
mcpResourceScopeDirect: "直接",
mcpResourceScopePartial: "一部許可",
mcpResourceScopeEmpty: "一致するグループまたは接続がありません。",
mcpResourceScopeHint: "選択したグループには現在および今後の子孫接続が含まれます。直接選択は独立した例外です。",
mcpResourceScopeAllConfirm: "すべての接続に切り替えると、現在のカスタムグループと接続ルールが削除されます。続行しますか?",
mcpPermissionGlobalDefaultHint: "グループ、接続、データベースの権限はこの既定値を上書きしますが、接続の表示範囲を拡張することはありません。",
mcpDatabasePolicyHighRiskConfirm: "このデータベースで高リスク書き込みを許可しますか?接続の読み取り専用および本番保護は引き続き適用されます。",
mcpDatabaseScopeClearsPermissionExceptions: "このデータベース範囲を変更すると、データベース権限の例外が削除されます。続行しますか?",
mcpPermissionPreviewResource: "リソース",
mcpPermissionPreviewSource: "適用元",
mcpPermissionSourceDatabase: "データベース例外",
mcpPermissionSourceConnection: "接続例外",
mcpPermissionSourceGroup: "グループ: {group}",
mcpPermissionSourceGlobal: "グローバル既定値",
mcpPerPage: "表示件数",
mcpPreviousPage: "前のページ",
mcpNextPage: "次のページ",
@@ -6847,7 +6870,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "データベースアクセスなし",
mcpDatabaseScopeSummarySelected: "{count} 件を選択済み",
mcpDatabaseScopePermissionNote: "範囲を選択しても、DBX 内のこの接続の既存のデータベース権限は変更されません。",
mcpDatabaseLoadButton: "接続から読み込む",
mcpDatabaseLoadButton: "リソース一覧を読み込む",
mcpDatabaseScopeAriaLabel: "データベースアクセス範囲",
mcpDatabaseScopeAllTitle: "すべてのデータベース",
mcpDatabaseScopeAllDescription: "現在のアクセス範囲を維持",
@@ -6860,10 +6883,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "データベース優先設定",
mcpDatabasePolicyEffective: "有効: {mode}",
mcpPermissionPreviewTitle: "権限プレビュー",
mcpPermissionPreviewDescription: "権限はスコープの優先順で決まります:データベース、接続既定、グローバル既定。",
mcpPermissionPreviewDescription: "権限の優先順:データベース、接続、最も近いグループ、上位グループ、グローバル。",
mcpPermissionPreviewConnection: "接続",
mcpPermissionPreviewDatabase: "データベース",
mcpPermissionPreviewConnectionDefault: "接続の既定",
mcpPermissionPreviewGroupDefault: "グループの既定",
mcpGroupScopeTitle: "接続グループ",
mcpGroupScopeDescription: "グループを選択すると、現在および今後追加されるすべての子孫接続が動的に公開されます。他の接続も個別に追加できます。",
mcpGroupScopeSelected: "{count} グループを選択",
mcpGroupScopeSearch: "グループパスを検索",
mcpGroupScopeNoResults: "一致するグループがありません。",
mcpGroupScopeEmpty: "設定可能な接続グループがありません。",
mcpGroupPolicyTitle: "グループの既定権限",
mcpGroupPolicyDescription: "子グループは親を上書きし、接続とデータベースの設定はグループを上書きできます。",
mcpGroupPolicyInherit: "親またはグローバルを継承",
mcpGroupPolicyHighRiskConfirm: "このグループに高リスク書き込みを許可しますか?接続の読み取り専用と本番保護は引き続き有効です。",
mcpPermissionPreviewDatabaseOverride: "データベース優先設定",
mcpPermissionPreviewEffective: "最終権限",
mcpPermissionPreviewEmpty: "先に少なくとも 1 つの接続を許可し、データベース範囲を設定してください。",
@@ -6872,7 +6906,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "読み込み済みデータベースを絞り込み",
mcpDatabaseManualPlaceholder: "データベース名を手動追加",
mcpDatabaseLoadFailed: "データベースの読み込みに失敗しました:{error}。正確なデータベース名を手動で追加できます。",
mcpDatabaseEmptyHint: "「接続から読み込む」をクリックしてデータベース一覧を取得するか、許可するデータベースを手動で追加してください。",
mcpDatabaseEmptyHint: "「リソース一覧を読み込む」をクリックしてデータベース一覧を取得するか、許可するデータベースを手動で追加してください。",
mcpManagementAccess: "接続方式",
mcpManagementPermissions: "権限管理",
mcpTransportLabel: "接続タイプ",
mcpRuntimeSettings: "実行設定",
mcpTransportLocalStdio: "ローカル stdio",
mcpTransportHttpService: "HTTP サービス",
mcpHttpWebServiceTitle: "Web Streamable HTTP サービス",
+47 -9
View File
@@ -6568,20 +6568,43 @@ export default withEnglishFallback({
mcpRefresh: "다시 확인",
mcpGuide: "MCP 가이드",
mcpAuthTitle: "MCP 권한 설정",
mcpAuthDescription: "순서대로 권한을 설정하세요: 먼저 연결을 선택하고, 데이터베이스와 연결 예외를 제한한 뒤, 마지막으로 사용할 도구와 전역 실행 수준을 확정합니다.",
mcpAuthDescription: "순서대로 권한을 설정하세요: 전역 기본값을 정하고 그룹과 연결, 데이터베이스 범위를 설정한 뒤 최종 정책을 확인합니다.",
mcpAuthStepsLabel: "MCP 권한 설정 단계",
mcpAuthStepConnectionsTitle: "연결 범위",
mcpAuthStepConnectionsTitle: "그룹 및 연결",
mcpAuthStepConnectionsDescription: "MCP에 노출할 연결을 결정합니다",
mcpAuthStepDatabasesTitle: "데이터베이스 범위",
mcpAuthStepDatabasesDescription: "연결별로 접근 가능한 데이터베이스를 제한합니다",
mcpAuthStepOverridesTitle: "연결 기본 권한",
mcpAuthStepOverridesDescription: "연결별 기본 실행 권한을 설정합니다",
mcpAuthStepCapabilitiesTitle: "기능 및 실행",
mcpAuthStepCapabilitiesDescription: "MCP 도구와 전역 기본 작업 수준을 선택합니다",
mcpAuthStepOverridesTitle: "기본 권한",
mcpAuthStepOverridesDescription: "이후 범위에 적용할 전역 기본 권한을 설정합니다",
mcpAuthStepCapabilitiesTitle: "요약 및 확인",
mcpAuthStepCapabilitiesDescription: "최종 권한을 검토하고 도구를 선택한 뒤 확인합니다",
mcpAuthStepRegionLabel: "단계 {step}: {title}",
mcpAuthPreviousStep: "이전",
mcpAuthNextStep: "다음",
mcpAuthStepProgress: "{total}단계 중 {current}단계",
mcpResourceScopeTitle: "MCP에 표시되는 연결",
mcpResourceScopeDescription: "동적 범위에는 그룹을 선택하고 정확한 예외에는 개별 연결을 선택하세요.",
mcpResourceScopeModeCustom: "사용자 지정 범위",
mcpResourceScopeAllSummary: "모든 {count}개 연결",
mcpResourceScopeCustomSummary: "유효 {effective}개 · 그룹 규칙 {groups}개 · 직접 규칙 {connections}개",
mcpResourceScopeSearch: "그룹 또는 연결 검색",
mcpResourceScopeTreeLabel: "MCP 연결 리소스 트리",
mcpResourceScopeInherited: "{group}에서 상속",
mcpResourceScopeDynamic: "동적",
mcpResourceScopeDirect: "직접",
mcpResourceScopePartial: "부분 허용",
mcpResourceScopeEmpty: "일치하는 그룹 또는 연결이 없습니다.",
mcpResourceScopeHint: "선택한 그룹에는 현재와 미래의 모든 하위 연결이 포함됩니다. 직접 선택은 독립적인 예외입니다.",
mcpResourceScopeAllConfirm: "모든 연결로 전환하면 현재 사용자 지정 그룹 및 연결 규칙이 제거됩니다. 계속하시겠습니까?",
mcpPermissionGlobalDefaultHint: "그룹, 연결 및 데이터베이스 권한은 이 기본값을 재정의하지만 연결 표시 범위를 확장하지는 않습니다.",
mcpDatabasePolicyHighRiskConfirm: "이 데이터베이스에 고위험 쓰기를 허용하시겠습니까? 연결 읽기 전용 및 운영 환경 보호는 계속 적용됩니다.",
mcpDatabaseScopeClearsPermissionExceptions: "이 데이터베이스 범위를 변경하면 데이터베이스 권한 예외가 제거됩니다. 계속하시겠습니까?",
mcpPermissionPreviewResource: "리소스",
mcpPermissionPreviewSource: "적용 출처",
mcpPermissionSourceDatabase: "데이터베이스 예외",
mcpPermissionSourceConnection: "연결 예외",
mcpPermissionSourceGroup: "그룹: {group}",
mcpPermissionSourceGlobal: "전역 기본값",
mcpPerPage: "페이지당",
mcpPreviousPage: "이전 페이지",
mcpNextPage: "다음 페이지",
@@ -6593,7 +6616,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "데이터베이스 접근 없음",
mcpDatabaseScopeSummarySelected: "{count}개 선택됨",
mcpDatabaseScopePermissionNote: "범위를 선택해도 DBX에서 이 연결의 기존 데이터베이스 권한은 변경되지 않습니다.",
mcpDatabaseLoadButton: "연결에서 불러오기",
mcpDatabaseLoadButton: "리소스 목록 불러오기",
mcpDatabaseScopeAriaLabel: "데이터베이스 접근 범위",
mcpDatabaseScopeAllTitle: "모든 데이터베이스",
mcpDatabaseScopeAllDescription: "현재 접근 범위 유지",
@@ -6606,10 +6629,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "데이터베이스 우선 설정",
mcpDatabasePolicyEffective: "적용됨: {mode}",
mcpPermissionPreviewTitle: "권한 미리보기",
mcpPermissionPreviewDescription: "권한은 범위 우선순위로 결정됩니다: 데이터베이스, 연결 기본값, 전역 기본값.",
mcpPermissionPreviewDescription: "권한 우선순위: 데이터베이스, 연결, 가장 가까운 그룹, 상위 그룹, 전역 기본값.",
mcpPermissionPreviewConnection: "연결",
mcpPermissionPreviewDatabase: "데이터베이스",
mcpPermissionPreviewConnectionDefault: "연결 기본값",
mcpPermissionPreviewGroupDefault: "그룹 기본값",
mcpGroupScopeTitle: "연결 그룹",
mcpGroupScopeDescription: "그룹을 선택하면 현재 및 향후의 모든 하위 연결이 동적으로 노출됩니다. 다른 연결도 개별적으로 추가할 수 있습니다.",
mcpGroupScopeSelected: "{count}개 그룹 선택됨",
mcpGroupScopeSearch: "그룹 경로 검색",
mcpGroupScopeNoResults: "일치하는 그룹이 없습니다.",
mcpGroupScopeEmpty: "구성할 연결 그룹이 없습니다.",
mcpGroupPolicyTitle: "그룹 기본 권한",
mcpGroupPolicyDescription: "하위 그룹은 상위 그룹을 재정의하며 연결 및 데이터베이스 설정은 그룹 기본값을 재정의할 수 있습니다.",
mcpGroupPolicyInherit: "상위 또는 전역 상속",
mcpGroupPolicyHighRiskConfirm: "이 그룹에 고위험 쓰기를 허용하시겠습니까? 연결 읽기 전용 및 프로덕션 보호는 계속 적용됩니다.",
mcpPermissionPreviewDatabaseOverride: "데이터베이스 우선 설정",
mcpPermissionPreviewEffective: "최종 적용",
mcpPermissionPreviewEmpty: "먼저 하나 이상의 연결을 허용하고 데이터베이스 범위를 설정하세요.",
@@ -6618,7 +6652,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "불러온 데이터베이스 필터",
mcpDatabaseManualPlaceholder: "데이터베이스 이름 직접 추가",
mcpDatabaseLoadFailed: "데이터베이스 불러오기 실패: {error}. 정확한 데이터베이스 이름을 직접 추가할 수 있습니다.",
mcpDatabaseEmptyHint: "'연결에서 불러오기'를 클릭해 데이터베이스 목록을 가져오거나, 허용할 데이터베이스를 직접 추가하세요.",
mcpDatabaseEmptyHint: "'리소스 목록 불러오기'를 클릭해 데이터베이스 목록을 가져오거나, 허용할 데이터베이스를 직접 추가하세요.",
mcpManagementAccess: "접속 방식",
mcpManagementPermissions: "권한 관리",
mcpTransportLabel: "연결 유형",
mcpRuntimeSettings: "실행 설정",
mcpTransportLocalStdio: "로컬 stdio",
mcpTransportHttpService: "HTTP 서비스",
mcpHttpWebServiceTitle: "Web Streamable HTTP 서비스",
+47 -9
View File
@@ -6817,20 +6817,43 @@ export default withEnglishFallback({
mcpRefresh: "Verificar novamente",
mcpGuide: "Guia do MCP",
mcpAuthTitle: "Autorização do MCP",
mcpAuthDescription: "Conclua a autorização em ordem: escolha as conexões primeiro, depois limite os bancos de dados e as exceções por conexão e, por fim, defina as ferramentas disponíveis e o nível de execução global.",
mcpAuthDescription: "Conclua a autorização em ordem: defina o padrão global, escolha grupos e conexões, limite os bancos e revise a política final.",
mcpAuthStepsLabel: "Etapas de autorização do MCP",
mcpAuthStepConnectionsTitle: "Escopo de conexões",
mcpAuthStepConnectionsTitle: "Grupos e conexões",
mcpAuthStepConnectionsDescription: "Define quais conexões o MCP pode ver",
mcpAuthStepDatabasesTitle: "Escopo de bancos de dados",
mcpAuthStepDatabasesDescription: "Limita os bancos acessíveis por conexão",
mcpAuthStepOverridesTitle: "Padrões de conexão",
mcpAuthStepOverridesDescription: "Define a permissão de execução padrão de cada conexão",
mcpAuthStepCapabilitiesTitle: "Capacidades e execução",
mcpAuthStepCapabilitiesDescription: "Escolha as ferramentas do MCP e o nível de operação global padrão",
mcpAuthStepOverridesTitle: "Permissões padrão",
mcpAuthStepOverridesDescription: "Defina a permissão global para os escopos seguintes",
mcpAuthStepCapabilitiesTitle: "Resumo e confirmação",
mcpAuthStepCapabilitiesDescription: "Revise as permissões efetivas, escolha as ferramentas e confirme",
mcpAuthStepRegionLabel: "Etapa {step}: {title}",
mcpAuthPreviousStep: "Voltar",
mcpAuthNextStep: "Avançar",
mcpAuthStepProgress: "Etapa {current} de {total}",
mcpResourceScopeTitle: "Conexões visíveis ao MCP",
mcpResourceScopeDescription: "Selecione grupos para cobertura dinâmica e conexões individuais para exceções precisas.",
mcpResourceScopeModeCustom: "Escopo personalizado",
mcpResourceScopeAllSummary: "Todas as {count} conexões",
mcpResourceScopeCustomSummary: "{effective} efetivas · {groups} regras de grupo · {connections} regras diretas",
mcpResourceScopeSearch: "Pesquisar grupos ou conexões",
mcpResourceScopeTreeLabel: "Árvore de recursos de conexões MCP",
mcpResourceScopeInherited: "De {group}",
mcpResourceScopeDynamic: "Dinâmico",
mcpResourceScopeDirect: "Direto",
mcpResourceScopePartial: "Parcialmente permitido",
mcpResourceScopeEmpty: "Nenhum grupo ou conexão correspondente.",
mcpResourceScopeHint: "Um grupo selecionado inclui suas conexões descendentes atuais e futuras. Seleções diretas são exceções independentes.",
mcpResourceScopeAllConfirm: "Mudar para todas as conexões remove as regras atuais de grupos e conexões personalizadas. Continuar?",
mcpPermissionGlobalDefaultHint: "As permissões de grupo, conexão e banco de dados substituem este padrão, mas nunca ampliam a visibilidade das conexões.",
mcpDatabasePolicyHighRiskConfirm: "Permitir gravações de alto risco para este banco de dados? As proteções de somente leitura e de produção da conexão continuam aplicadas.",
mcpDatabaseScopeClearsPermissionExceptions: "Alterar este escopo de banco de dados removerá suas exceções de permissão. Continuar?",
mcpPermissionPreviewResource: "Recurso",
mcpPermissionPreviewSource: "Origem",
mcpPermissionSourceDatabase: "Exceção de banco de dados",
mcpPermissionSourceConnection: "Exceção de conexão",
mcpPermissionSourceGroup: "Grupo: {group}",
mcpPermissionSourceGlobal: "Padrão global",
mcpPerPage: "Por página",
mcpPreviousPage: "Página anterior",
mcpNextPage: "Próxima página",
@@ -6842,7 +6865,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "Sem acesso a bancos",
mcpDatabaseScopeSummarySelected: "{count} selecionados",
mcpDatabaseScopePermissionNote: "O escopo selecionado não altera as permissões de bancos que a conexão já tem no DBX.",
mcpDatabaseLoadButton: "Carregar da conexão",
mcpDatabaseLoadButton: "Carregar lista de recursos",
mcpDatabaseScopeAriaLabel: "Escopo de acesso aos bancos",
mcpDatabaseScopeAllTitle: "Todos os bancos de dados",
mcpDatabaseScopeAllDescription: "Mantém o escopo de acesso atual",
@@ -6855,10 +6878,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "Configuração prioritária do banco",
mcpDatabasePolicyEffective: "Efetivo: {mode}",
mcpPermissionPreviewTitle: "Prévia de permissões",
mcpPermissionPreviewDescription: "As permissões seguem a prioridade de escopo: banco, padrão da conexão e padrão global.",
mcpPermissionPreviewDescription: "Prioridade: banco, conexão, grupo mais próximo, grupo superior e padrão global.",
mcpPermissionPreviewConnection: "Conexão",
mcpPermissionPreviewDatabase: "Banco de dados",
mcpPermissionPreviewConnectionDefault: "Padrão da conexão",
mcpPermissionPreviewGroupDefault: "Padrão do grupo",
mcpGroupScopeTitle: "Grupos de conexões",
mcpGroupScopeDescription: "Selecionar um grupo expõe dinamicamente todas as conexões descendentes atuais e futuras. Outras conexões ainda podem ser adicionadas explicitamente.",
mcpGroupScopeSelected: "{count} grupos selecionados",
mcpGroupScopeSearch: "Buscar caminhos de grupos",
mcpGroupScopeNoResults: "Nenhum grupo correspondente.",
mcpGroupScopeEmpty: "Não há grupos de conexões para configurar.",
mcpGroupPolicyTitle: "Permissões padrão do grupo",
mcpGroupPolicyDescription: "Um grupo filho substitui o pai; conexão e banco de dados podem substituir o grupo.",
mcpGroupPolicyInherit: "Herdar do pai ou global",
mcpGroupPolicyHighRiskConfirm: "Permitir gravações de alto risco para este grupo? As proteções de somente leitura e produção continuam ativas.",
mcpPermissionPreviewDatabaseOverride: "Configuração prioritária do banco",
mcpPermissionPreviewEffective: "Efetiva",
mcpPermissionPreviewEmpty: "Permita primeiro ao menos uma conexão e configure seu escopo de bancos.",
@@ -6867,7 +6901,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "Filtrar bancos carregados",
mcpDatabaseManualPlaceholder: "Adicionar nome do banco manualmente",
mcpDatabaseLoadFailed: "Falha ao carregar bancos: {error}. Você pode adicionar manualmente os nomes exatos.",
mcpDatabaseEmptyHint: "Clique em 'Carregar da conexão' para obter a lista de bancos ou adicione manualmente os permitidos.",
mcpDatabaseEmptyHint: "Clique em 'Carregar lista de recursos' para obter a lista de bancos ou adicione manualmente os permitidos.",
mcpManagementAccess: "Formas de acesso",
mcpManagementPermissions: "Gerenciamento de permissões",
mcpTransportLabel: "Tipo de conexão",
mcpRuntimeSettings: "Configurações de execução",
mcpTransportLocalStdio: "stdio local",
mcpTransportHttpService: "Serviço HTTP",
mcpHttpWebServiceTitle: "Serviço Web Streamable HTTP",
+47 -9
View File
@@ -7184,20 +7184,37 @@ export default withEnglishFallback({
mcpRefresh: "Yeniden denetle",
mcpGuide: "MCP kılavuzu",
mcpAuthTitle: "MCP yetkilendirmesi",
mcpAuthDescription: "Yetkilendirmeyi sırayla tamamlayın: önce bağlantıları seçin, sonra veritabanlarını ve bağlantı istisnalarını sınırlayın, en son kullanılabilir araçları ve genel yürütme düzeyini belirleyin.",
mcpAuthDescription: "Yetkilendirmeyi sırayla tamamlayın: genel varsayılanı belirleyin, grup ve bağlantıları seçin, veritabanlarını sınırlayın ve son politikayı inceleyin.",
mcpAuthStepsLabel: "MCP yetkilendirme adımları",
mcpAuthStepConnectionsTitle: "Bağlantı kapsamı",
mcpAuthStepConnectionsTitle: "Gruplar ve bağlantılar",
mcpAuthStepConnectionsDescription: "MCP'nin hangi bağlantıları görebileceğini belirler",
mcpAuthStepDatabasesTitle: "Veritabanı kapsamı",
mcpAuthStepDatabasesDescription: "Her bağlantının erişebileceği veritabanlarını sınırlayın",
mcpAuthStepOverridesTitle: "Bağlantı varsayılanları",
mcpAuthStepOverridesDescription: "Her bağlantı için varsayılan yürütme yetkisini belirleyin",
mcpAuthStepCapabilitiesTitle: "Yetenekler ve yürütme",
mcpAuthStepCapabilitiesDescription: "MCP araçlarını ve genel varsayılan işlem düzeyini seçin",
mcpAuthStepOverridesTitle: "Varsayılan izinler",
mcpAuthStepOverridesDescription: "Sonraki kapsamlar için genel varsayılan izni belirleyin",
mcpAuthStepCapabilitiesTitle: "Özet ve onay",
mcpAuthStepCapabilitiesDescription: "Etkin izinleri inceleyin, araçları seçin ve onaylayın",
mcpAuthStepRegionLabel: "Adım {step}: {title}",
mcpAuthPreviousStep: "Geri",
mcpAuthNextStep: "İleri",
mcpAuthStepProgress: "{total} adımdan {current}. adım",
mcpResourceScopeTitle: "MCP'ye görünür bağlantılar",
mcpResourceScopeDescription: "Dinamik kapsam için grupları, hassas istisnalar için tek tek bağlantıları seçin.",
mcpResourceScopeModeCustom: "Özel kapsam",
mcpResourceScopeAllSummary: "Tüm {count} bağlantı",
mcpResourceScopeCustomSummary: "{effective} etkin · {groups} grup kuralı · {connections} doğrudan kural",
mcpResourceScopeSearch: "Grup veya bağlantı ara",
mcpResourceScopeTreeLabel: "MCP bağlantı kaynak ağacı",
mcpResourceScopeInherited: "{group} grubundan",
mcpResourceScopeDynamic: "Dinamik",
mcpResourceScopeDirect: "Doğrudan",
mcpResourceScopePartial: "Kısmen izinli",
mcpResourceScopeEmpty: "Eşleşen grup veya bağlantı yok.",
mcpResourceScopeHint: "Seçilen bir grup, mevcut ve gelecekteki alt bağlantılarını kapsar. Doğrudan seçimler bağımsız istisnalardır.",
mcpResourceScopeAllConfirm: "Tüm bağlantılara geçmek, mevcut özel grup ve bağlantı kurallarını kaldırır. Devam edilsin mi?",
mcpPermissionGlobalDefaultHint: "Grup, bağlantı ve veritabanı izinleri bu varsayılanı geçersiz kılar; ancak bağlantı görünürlüğünü asla genişletmez.",
mcpDatabasePolicyHighRiskConfirm: "Bu veritabanı için yüksek riskli yazmalara izin verilsin mi? Bağlantı salt okunur ve üretim korumaları yine geçerlidir.",
mcpDatabaseScopeClearsPermissionExceptions: "Bu veritabanı kapsamını değiştirmek, veritabanı izin istisnalarını kaldırır. Devam edilsin mi?",
mcpPerPage: "Sayfa başına",
mcpPreviousPage: "Önceki sayfa",
mcpNextPage: "Sonraki sayfa",
@@ -7209,7 +7226,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "Veritabanı erişimi yok",
mcpDatabaseScopeSummarySelected: "{count} seçildi",
mcpDatabaseScopePermissionNote: "Seçili kapsam, bağlantının DBX'teki mevcut veritabanı yetkilerini değiştirmez.",
mcpDatabaseLoadButton: "Bağlantıdan yükle",
mcpDatabaseLoadButton: "Kaynak listesini yükle",
mcpDatabaseScopeAriaLabel: "Veritabanı erişim kapsamı",
mcpDatabaseScopeAllTitle: "Tüm veritabanları",
mcpDatabaseScopeAllDescription: "Geçerli erişim kapsamını koru",
@@ -7222,19 +7239,40 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "Veritabanı önceliği ayarı",
mcpDatabasePolicyEffective: "Geçerli: {mode}",
mcpPermissionPreviewTitle: "Yetki önizlemesi",
mcpPermissionPreviewDescription: "Yetkiler kapsama göre geri dönüş yapar: önce veritabanı ayarı, sonra bağlantı varsayılanı, sonra genel varsayılan.",
mcpPermissionPreviewDescription: "Öncelik: veritabanı, bağlantı, en yakın grup, üst grup ve genel varsayılan.",
mcpPermissionPreviewConnection: "Bağlantı",
mcpPermissionPreviewDatabase: "Veritabanı",
mcpPermissionPreviewConnectionDefault: "Bağlantı varsayılanı",
mcpPermissionPreviewGroupDefault: "Grup varsayılanı",
mcpGroupScopeTitle: "Bağlantı grupları",
mcpGroupScopeDescription: "Bir grup seçildiğinde mevcut ve gelecekteki tüm alt bağlantılar dinamik olarak açılır. Diğer bağlantılar ayrıca eklenebilir.",
mcpGroupScopeSelected: "{count} grup seçildi",
mcpGroupScopeSearch: "Grup yollarını ara",
mcpGroupScopeNoResults: "Eşleşen grup bulunamadı.",
mcpGroupScopeEmpty: "Yapılandırılacak bağlantı grubu yok.",
mcpGroupPolicyTitle: "Grup varsayılan izinleri",
mcpGroupPolicyDescription: "Alt grup üst grubu geçersiz kılar; bağlantı ve veritabanı ayarları grup varsayılanını geçersiz kılabilir.",
mcpGroupPolicyInherit: "Üst veya genelden devral",
mcpGroupPolicyHighRiskConfirm: "Bu grup için yüksek riskli yazmalara izin verilsin mi? Salt okunur ve üretim korumaları geçerli kalır.",
mcpPermissionPreviewDatabaseOverride: "Veritabanı önceliği ayarı",
mcpPermissionPreviewEffective: "Geçerli",
mcpPermissionPreviewResource: "Kaynak",
mcpPermissionPreviewSource: "İzin kaynağı",
mcpPermissionSourceDatabase: "Veritabanı istisnası",
mcpPermissionSourceConnection: "Bağlantı istisnası",
mcpPermissionSourceGroup: "Grup: {group}",
mcpPermissionSourceGlobal: "Genel varsayılan",
mcpPermissionPreviewEmpty: "Önce en az bir bağlantıya izin verin ve veritabanı kapsamını yapılandırın.",
mcpPermissionPreviewSearchPlaceholder: "Bağlantı veya veritabanı ara",
mcpPermissionPreviewSearchNoResults: "Eşleşen bağlantı ya da veritabanı bulunamadı.",
mcpDatabaseSearchPlaceholder: "Yüklenen veritabanlarını filtrele",
mcpDatabaseManualPlaceholder: "Elle bir veritabanı adı ekle",
mcpDatabaseLoadFailed: "Veritabanları yüklenemedi: {error}. Veritabanı adlarını tam olarak elle ekleyebilirsiniz.",
mcpDatabaseEmptyHint: "Veritabanı listesini almak için 'Bağlantıdan yükle'ye tıklayın ya da izin verilen veritabanlarını elle ekleyin.",
mcpDatabaseEmptyHint: "Veritabanı listesini almak için 'Kaynak listesini yükle'ye tıklayın ya da izin verilen veritabanlarını elle ekleyin.",
mcpManagementAccess: "Erişim yöntemleri",
mcpManagementPermissions: "İzin yönetimi",
mcpTransportLabel: "Bağlantı türü",
mcpRuntimeSettings: "Çalışma zamanı ayarları",
mcpTransportLocalStdio: "Yerel stdio",
mcpTransportHttpService: "HTTP servisi",
mcpHttpWebServiceTitle: "Web Streamable HTTP servisi",
+49 -11
View File
@@ -7255,20 +7255,37 @@ export default withEnglishFallback({
mcpRefresh: "重新检查",
mcpGuide: "MCP 指南",
mcpAuthTitle: "MCP 授权设置",
mcpAuthDescription: "按顺序完成授权:先选择连接,再限定数据库和连接例外,最后确定可用工具与全局执行级别。",
mcpAuthDescription: "按顺序完成授权:先设置全局默认权限,再选择分组与连接、限定数据库,最后确认最终策略。",
mcpAuthStepsLabel: "MCP 授权步骤",
mcpAuthStepConnectionsTitle: "连接范围",
mcpAuthStepConnectionsDescription: "决定哪些连接可被 MCP 看见",
mcpAuthStepConnectionsTitle: "分组与连接",
mcpAuthStepConnectionsDescription: "决定哪些分组和连接可被 MCP 看见,并设置连接权限",
mcpAuthStepDatabasesTitle: "数据库范围",
mcpAuthStepDatabasesDescription: "为每个连接限定可访问的库",
mcpAuthStepOverridesTitle: "连接默认权限",
mcpAuthStepOverridesDescription: "为每个连接设置默认执行权限",
mcpAuthStepCapabilitiesTitle: "能力与执行",
mcpAuthStepCapabilitiesDescription: "选择 MCP 工具和全局默认操作级别",
mcpAuthStepDatabasesDescription: "为每个连接限定可访问的库,并设置单库权限",
mcpAuthStepOverridesTitle: "默认权限",
mcpAuthStepOverridesDescription: "设置全局默认权限,后续范围可按需覆盖",
mcpAuthStepCapabilitiesTitle: "汇总与确认",
mcpAuthStepCapabilitiesDescription: "查看最终生效权限、选择 MCP 工具并确认策略",
mcpAuthStepRegionLabel: "第 {step} 步:{title}",
mcpAuthPreviousStep: "上一步",
mcpAuthNextStep: "下一步",
mcpAuthStepProgress: "第 {current} 步,共 {total} 步",
mcpResourceScopeTitle: "MCP 可见的连接",
mcpResourceScopeDescription: "选择分组可动态覆盖其下连接,也可单独选择连接作为精确例外。",
mcpResourceScopeModeCustom: "自定义范围",
mcpResourceScopeAllSummary: "全部 {count} 个连接",
mcpResourceScopeCustomSummary: "最终 {effective} 个连接 · {groups} 条分组规则 · {connections} 条单连接规则",
mcpResourceScopeSearch: "搜索分组或连接",
mcpResourceScopeTreeLabel: "MCP 连接资源树",
mcpResourceScopeInherited: "来自 {group}",
mcpResourceScopeDynamic: "动态包含",
mcpResourceScopeDirect: "单独授权",
mcpResourceScopePartial: "部分授权",
mcpResourceScopeEmpty: "没有匹配的分组或连接。",
mcpResourceScopeHint: "选中分组会包含当前及未来新增的所有下级连接;单连接授权是独立例外。",
mcpResourceScopeAllConfirm: "切换到全部连接会清除当前的自定义分组和单连接规则,是否继续?",
mcpPermissionGlobalDefaultHint: "分组、连接和数据库权限会覆盖此默认值,但不会扩大连接可见范围。",
mcpDatabasePolicyHighRiskConfirm: "确认允许此数据库执行高风险写操作吗?连接只读与生产库保护仍会生效。",
mcpDatabaseScopeClearsPermissionExceptions: "切换数据库范围会删除当前连接已有的单库权限例外,是否继续?",
mcpPerPage: "每页",
mcpPreviousPage: "上一页",
mcpNextPage: "下一页",
@@ -7280,7 +7297,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "无库访问",
mcpDatabaseScopeSummarySelected: "已选 {count} 个库",
mcpDatabaseScopePermissionNote: "选择范围不会改变此连接在 DBX 中原有的数据库权限。",
mcpDatabaseLoadButton: "从连接加载",
mcpDatabaseLoadButton: "加载资源列表",
mcpDatabaseScopeAriaLabel: "数据库访问范围",
mcpDatabaseScopeAllTitle: "全部数据库",
mcpDatabaseScopeAllDescription: "保持现有访问范围",
@@ -7293,19 +7310,40 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "单库优先设置",
mcpDatabasePolicyEffective: "生效:{mode}",
mcpPermissionPreviewTitle: "权限预览",
mcpPermissionPreviewDescription: "权限按范围由小到大回退:单库设置优先,其次连接默认,最后全局默认。",
mcpPermissionPreviewDescription: "权限优先级:单库设置、连接默认、最近的分组默认、上级分组默认、全局默认。",
mcpPermissionPreviewConnection: "连接",
mcpPermissionPreviewDatabase: "数据库",
mcpPermissionPreviewConnectionDefault: "连接默认",
mcpPermissionPreviewGroupDefault: "分组默认",
mcpGroupScopeTitle: "连接分组",
mcpGroupScopeDescription: "选择分组后,将动态开放其中当前及未来新增的所有下级连接;仍可单独添加其他连接。",
mcpGroupScopeSelected: "已选 {count} 个分组",
mcpGroupScopeSearch: "搜索分组路径",
mcpGroupScopeNoResults: "未找到匹配的分组。",
mcpGroupScopeEmpty: "当前没有可配置的连接分组。",
mcpGroupPolicyTitle: "分组默认权限",
mcpGroupPolicyDescription: "子分组覆盖父分组;连接和数据库设置可继续覆盖分组默认值。",
mcpGroupPolicyInherit: "继承上级或全局",
mcpGroupPolicyHighRiskConfirm: "确认允许此分组执行高风险写操作吗?连接只读与生产库保护仍会生效。",
mcpPermissionPreviewDatabaseOverride: "单库优先设置",
mcpPermissionPreviewEffective: "最终生效",
mcpPermissionPreviewResource: "资源",
mcpPermissionPreviewSource: "权限来源",
mcpPermissionSourceDatabase: "数据库例外",
mcpPermissionSourceConnection: "连接例外",
mcpPermissionSourceGroup: "分组:{group}",
mcpPermissionSourceGlobal: "全局默认",
mcpPermissionPreviewEmpty: "请先允许至少一个连接,并配置可访问的数据库范围。",
mcpPermissionPreviewSearchPlaceholder: "搜索连接或数据库",
mcpPermissionPreviewSearchNoResults: "未找到匹配的连接或数据库。",
mcpDatabaseSearchPlaceholder: "筛选已加载的数据库",
mcpDatabaseManualPlaceholder: "手工添加库名",
mcpDatabaseLoadFailed: "加载数据库失败:{error}。可手工添加准确的数据库名称。",
mcpDatabaseEmptyHint: "点击“从连接加载”获取数据库列表,或手工添加允许的数据库。",
mcpDatabaseEmptyHint: "点击“加载资源列表”获取数据库列表,或手工添加允许的数据库。",
mcpManagementAccess: "接入方式",
mcpManagementPermissions: "权限管理",
mcpTransportLabel: "接入类型",
mcpRuntimeSettings: "运行参数",
mcpTransportLocalStdio: "本地 stdio",
mcpTransportHttpService: "HTTP 服务",
mcpHttpWebServiceTitle: "Web Streamable HTTP 服务",
+47 -9
View File
@@ -6150,20 +6150,43 @@ export default withEnglishFallback({
mcpRefresh: "重新檢查",
mcpGuide: "MCP 指南",
mcpAuthTitle: "MCP 授權設定",
mcpAuthDescription: "按順序完成授權:先選擇連線,再限定資料庫和連線例外,最後確定可用工具與全域執行層級。",
mcpAuthDescription: "按順序完成授權:先設定全域預設權限,再選擇群組與連線、限定資料庫,最後確認最終策略。",
mcpAuthStepsLabel: "MCP 授權步驟",
mcpAuthStepConnectionsTitle: "連線範圍",
mcpAuthStepConnectionsTitle: "群組與連線",
mcpAuthStepConnectionsDescription: "決定哪些連線可被 MCP 看見",
mcpAuthStepDatabasesTitle: "資料庫範圍",
mcpAuthStepDatabasesDescription: "為每個連線限定可存取的庫",
mcpAuthStepOverridesTitle: "連線預設權限",
mcpAuthStepOverridesDescription: "為每個連線設定預設執行權限",
mcpAuthStepCapabilitiesTitle: "能力與執行",
mcpAuthStepCapabilitiesDescription: "選擇 MCP 工具和全域預設操作層級",
mcpAuthStepOverridesTitle: "預設權限",
mcpAuthStepOverridesDescription: "設定套用至後續範圍的全域預設權限",
mcpAuthStepCapabilitiesTitle: "摘要與確認",
mcpAuthStepCapabilitiesDescription: "檢視最終權限、選擇工具並確認策略",
mcpAuthStepRegionLabel: "第 {step} 步:{title}",
mcpAuthPreviousStep: "上一步",
mcpAuthNextStep: "下一步",
mcpAuthStepProgress: "第 {current} 步,共 {total} 步",
mcpResourceScopeTitle: "MCP 可見的連線",
mcpResourceScopeDescription: "選擇群組可動態涵蓋其下連線,也可單獨選擇連線作為精確例外。",
mcpResourceScopeModeCustom: "自訂範圍",
mcpResourceScopeAllSummary: "全部 {count} 個連線",
mcpResourceScopeCustomSummary: "最終 {effective} 個連線 · {groups} 條群組規則 · {connections} 條單連線規則",
mcpResourceScopeSearch: "搜尋群組或連線",
mcpResourceScopeTreeLabel: "MCP 連線資源樹",
mcpResourceScopeInherited: "來自 {group}",
mcpResourceScopeDynamic: "動態包含",
mcpResourceScopeDirect: "單獨授權",
mcpResourceScopePartial: "部分授權",
mcpResourceScopeEmpty: "沒有符合的群組或連線。",
mcpResourceScopeHint: "選取群組會包含目前及未來新增的所有下層連線;單連線授權是獨立例外。",
mcpResourceScopeAllConfirm: "切換到全部連線會清除目前的自訂群組和單連線規則,是否繼續?",
mcpPermissionGlobalDefaultHint: "群組、連線和資料庫權限會覆寫此預設值,但不會擴大連線可見範圍。",
mcpDatabasePolicyHighRiskConfirm: "確認允許此資料庫執行高風險寫入操作嗎?連線唯讀與正式環境保護仍會生效。",
mcpDatabaseScopeClearsPermissionExceptions: "切換資料庫範圍會移除目前的資料庫權限例外,是否繼續?",
mcpPermissionPreviewResource: "資源",
mcpPermissionPreviewSource: "權限來源",
mcpPermissionSourceDatabase: "資料庫例外",
mcpPermissionSourceConnection: "連線例外",
mcpPermissionSourceGroup: "群組:{group}",
mcpPermissionSourceGlobal: "全域預設",
mcpPerPage: "每頁",
mcpPreviousPage: "上一頁",
mcpNextPage: "下一頁",
@@ -6175,7 +6198,7 @@ export default withEnglishFallback({
mcpDatabaseScopeSummaryNone: "無庫存取",
mcpDatabaseScopeSummarySelected: "已選 {count} 個庫",
mcpDatabaseScopePermissionNote: "選擇範圍不會改變此連線在 DBX 中原有的資料庫權限。",
mcpDatabaseLoadButton: "從連線載入",
mcpDatabaseLoadButton: "載入資源清單",
mcpDatabaseScopeAriaLabel: "資料庫存取範圍",
mcpDatabaseScopeAllTitle: "全部資料庫",
mcpDatabaseScopeAllDescription: "保持現有存取範圍",
@@ -6188,10 +6211,21 @@ export default withEnglishFallback({
mcpDatabasePolicySetting: "單庫優先設定",
mcpDatabasePolicyEffective: "生效:{mode}",
mcpPermissionPreviewTitle: "權限預覽",
mcpPermissionPreviewDescription: "權限依範圍由小到大回退:單庫設定優先,其次連線預設,最後全域預設。",
mcpPermissionPreviewDescription: "權限優先順序:單庫設定、連線預設、最近的群組預設、上層群組預設、全域預設。",
mcpPermissionPreviewConnection: "連線",
mcpPermissionPreviewDatabase: "資料庫",
mcpPermissionPreviewConnectionDefault: "連線預設",
mcpPermissionPreviewGroupDefault: "群組預設",
mcpGroupScopeTitle: "連線群組",
mcpGroupScopeDescription: "選取群組後,會動態開放其中目前及未來新增的所有下層連線;仍可個別新增其他連線。",
mcpGroupScopeSelected: "已選 {count} 個群組",
mcpGroupScopeSearch: "搜尋群組路徑",
mcpGroupScopeNoResults: "找不到相符的群組。",
mcpGroupScopeEmpty: "目前沒有可設定的連線群組。",
mcpGroupPolicyTitle: "群組預設權限",
mcpGroupPolicyDescription: "子群組會覆寫父群組;連線與資料庫設定可繼續覆寫群組預設值。",
mcpGroupPolicyInherit: "繼承上層或全域",
mcpGroupPolicyHighRiskConfirm: "確定允許此群組執行高風險寫入嗎?連線唯讀與正式資料庫保護仍會生效。",
mcpPermissionPreviewDatabaseOverride: "單庫優先設定",
mcpPermissionPreviewEffective: "最終生效",
mcpPermissionPreviewEmpty: "請先允許至少一個連線,並設定可存取的資料庫範圍。",
@@ -6200,7 +6234,11 @@ export default withEnglishFallback({
mcpDatabaseSearchPlaceholder: "篩選已載入的資料庫",
mcpDatabaseManualPlaceholder: "手動新增庫名",
mcpDatabaseLoadFailed: "載入資料庫失敗:{error}。可手動新增準確的資料庫名稱。",
mcpDatabaseEmptyHint: "點擊「從連線載入」取得資料庫清單,或手動新增允許的資料庫。",
mcpDatabaseEmptyHint: "點擊「載入資源清單」取得資料庫清單,或手動新增允許的資料庫。",
mcpManagementAccess: "接入方式",
mcpManagementPermissions: "權限管理",
mcpTransportLabel: "接入類型",
mcpRuntimeSettings: "執行參數",
mcpTransportLocalStdio: "本機 stdio",
mcpTransportHttpService: "HTTP 服務",
mcpHttpWebServiceTitle: "Web Streamable HTTP 服務",
+13 -14
View File
@@ -81,7 +81,7 @@ describe("MCP policy settings state", () => {
it("guards the mutation entry point and wires the shared disabled state to policy controls", () => {
expect(settingsDialogSource).toContain("if (mcpPolicyControlsDisabled.value) return;");
expect(settingsDialogSource).toContain(':disabled="mcpPolicyControlsDisabled"');
expect(settingsDialogSource).toContain('@update:allowed-connection-ids="onMcpAllowedConnectionIdsChange"');
expect(settingsDialogSource).toContain('@update:scope="onMcpResourceScopeChange"');
const loadingStart = settingsDialogSource.indexOf("mcpPolicyLoading.value = true;");
const policyLoad = settingsDialogSource.indexOf("await settingsStore.initMcpGlobalPolicy(true);");
@@ -91,23 +91,22 @@ describe("MCP policy settings state", () => {
expect(loadingEnd).toBeGreaterThan(policyLoad);
});
it("keeps translated mode descriptions in one responsive layout track", () => {
const descriptionStart = settingsDialogSource.indexOf("data-mcp-execution-mode-description");
const descriptionEnd = settingsDialogSource.indexOf("settings.mcpCapabilityTitle", descriptionStart);
const descriptionSource = settingsDialogSource.slice(descriptionStart, descriptionEnd);
expect(descriptionStart).toBeGreaterThan(-1);
expect(descriptionEnd).toBeGreaterThan(descriptionStart);
expect(descriptionSource).toContain('class="grid text-xs"');
expect(descriptionSource.match(/col-start-1 row-start-1/g)).toHaveLength(3);
expect(descriptionSource.match(/\? 'visible' : 'invisible'/g)).toHaveLength(3);
it("renders one static mode hint in place of stacked per-mode description tracks", () => {
expect(settingsDialogSource).not.toContain("data-mcp-execution-mode-description");
const groupStart = settingsDialogSource.indexOf('role="radiogroup" aria-labelledby="mcp-execution-mode-label"');
const hintIndex = settingsDialogSource.indexOf("settings.mcpPermissionGlobalDefaultHint");
expect(groupStart).toBeGreaterThan(-1);
expect(hintIndex).toBeGreaterThan(groupStart);
});
it("keeps execution mode cards accessible as a keyboard radio group", () => {
expect(settingsDialogSource).toContain('role="radiogroup" aria-labelledby="mcp-execution-mode-label"');
expect(settingsDialogSource.match(/role="radio"/g)).toHaveLength(3);
expect(settingsDialogSource).toContain(":aria-checked=\"mcpExecutionMode === 'safe_write'\"");
expect(settingsDialogSource).toContain("onMcpExecutionModeKeydown($event, 'safe_write')");
expect(settingsDialogSource).toContain('v-for="mode in mcpExecutionModeOptions"');
expect(settingsDialogSource.match(/role="radio"/g)).toHaveLength(1);
expect(settingsDialogSource).toContain(':aria-checked="mcpExecutionMode === mode"');
expect(settingsDialogSource).toContain(':tabindex="mcpExecutionMode === mode ? 0 : -1"');
expect(settingsDialogSource).toContain("onMcpExecutionModeKeydown($event, mode)");
expect(settingsDialogSource).toContain('@keydown="onMcpExecutionModeKeydown($event, mode)"');
});
it("keeps MCP client config tabs on a single scrollable row", () => {
+8
View File
@@ -250,12 +250,20 @@ export interface McpGlobalPolicy {
readOnly: boolean;
allowDangerousSql: boolean;
allowedConnectionIds: string[] | null;
allowedGroupIds: string[];
allowedToolNames: string[] | null;
connectionPolicies: McpConnectionPolicy[];
groupPolicies: McpGroupPolicy[];
configured: boolean;
queryTimeoutSecs: number | null;
}
export interface McpGroupPolicy {
groupId: string;
readOnly: boolean;
allowDangerousSql: boolean;
}
export interface McpConnectionPolicy {
connectionId: string;
readOnly: boolean;
@@ -337,6 +337,24 @@ export function buildConnectionGroupPathMap(layout: SidebarLayout): Map<string,
return paths;
}
/** Build root-to-leaf stable group-id paths for permission inheritance. */
export function buildConnectionGroupIdPathMap(layout: SidebarLayout): Map<string, string[]> {
const paths = new Map<string, string[]>();
const visit = (entries: SidebarOrderEntry[], path: string[]) => {
for (const entry of entries) {
if (entry.type === "connection") {
paths.set(entry.id, path);
continue;
}
visit(entryChildren(entry), [...path, entry.id]);
}
};
visit(layout.order, []);
return paths;
}
export interface ConnectionGroupDestinationRow {
id: string;
name: string;
@@ -493,8 +493,10 @@ describe("normalizeMcpGlobalPolicy", () => {
readOnly: false,
allowDangerousSql: false,
allowedConnectionIds: null,
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: false,
queryTimeoutSecs: null,
});
@@ -512,8 +514,10 @@ describe("normalizeMcpGlobalPolicy", () => {
readOnly: true,
allowDangerousSql: true,
allowedConnectionIds: ["connection-1", "connection-2"],
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: true,
queryTimeoutSecs: null,
});
@@ -811,8 +815,10 @@ describe("settingsStore MCP policy persistence", () => {
readOnly: true,
allowDangerousSql: false,
allowedConnectionIds: ["connection-1"],
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: true,
queryTimeoutSecs: null,
};
@@ -826,8 +832,10 @@ describe("settingsStore MCP policy persistence", () => {
readOnly: false,
allowDangerousSql: false,
allowedConnectionIds: [],
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: true,
queryTimeoutSecs: null,
});
+29
View File
@@ -53,13 +53,21 @@ export interface McpGlobalPolicy {
readOnly: boolean;
allowDangerousSql: boolean;
allowedConnectionIds: string[] | null;
allowedGroupIds: string[];
allowedToolNames: string[] | null;
connectionPolicies: McpConnectionPolicy[];
groupPolicies: McpGroupPolicy[];
configured: boolean;
/** MCP query timeout override in seconds. null/undefined = inherit the connection; 0 = no limit. */
queryTimeoutSecs: number | null;
}
export interface McpGroupPolicy {
groupId: string;
readOnly: boolean;
allowDangerousSql: boolean;
}
export interface McpConnectionPolicy {
connectionId: string;
readOnly: boolean;
@@ -113,14 +121,17 @@ export const DEFAULT_MCP_GLOBAL_POLICY: McpGlobalPolicy = {
readOnly: false,
allowDangerousSql: false,
allowedConnectionIds: null,
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: false,
queryTimeoutSecs: null,
};
export function normalizeMcpGlobalPolicy(policy: Partial<McpGlobalPolicy> | null | undefined): McpGlobalPolicy {
const allowedConnectionIds = policy?.allowedConnectionIds === null || policy?.allowedConnectionIds === undefined ? null : [...new Set(policy.allowedConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
const allowedGroupIds = allowedConnectionIds === null ? [] : [...new Set((policy?.allowedGroupIds ?? []).filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
const allowedToolNames = policy?.allowedToolNames === null || policy?.allowedToolNames === undefined ? null : [...new Set(policy.allowedToolNames.filter((name): name is string => typeof name === "string" && name.trim().length > 0).map((name) => name.trim()))];
const connectionPolicies = Object.values(
(policy?.connectionPolicies ?? []).reduce<Record<string, McpConnectionPolicy>>((rules, rule) => {
@@ -156,6 +167,20 @@ export function normalizeMcpGlobalPolicy(policy: Partial<McpGlobalPolicy> | null
return rules;
}, {}),
);
const groupPolicies = Object.values(
(policy?.groupPolicies ?? []).reduce<Record<string, McpGroupPolicy>>((rules, rule) => {
if (!rule || typeof rule.groupId !== "string" || !rule.groupId.trim()) return rules;
const groupId = rule.groupId.trim();
const current = rules[groupId];
const readOnly = current?.readOnly === true || rule.readOnly === true;
rules[groupId] = {
groupId,
readOnly,
allowDangerousSql: !readOnly && (current ? current.allowDangerousSql && rule.allowDangerousSql === true : rule.allowDangerousSql === true),
};
return rules;
}, {}),
);
// null / undefined / non-positive => null (inherit connection). 0 is preserved
// as an explicit "no limit" only here; the UI maps "no limit" <=> 0 and
// "inherit" <=> null.
@@ -164,8 +189,10 @@ export function normalizeMcpGlobalPolicy(policy: Partial<McpGlobalPolicy> | null
readOnly: policy?.readOnly === true,
allowDangerousSql: policy?.allowDangerousSql === true,
allowedConnectionIds,
allowedGroupIds,
allowedToolNames,
connectionPolicies,
groupPolicies,
configured: policy?.configured === true,
queryTimeoutSecs,
};
@@ -1783,8 +1810,10 @@ export const useSettingsStore = defineStore("settings", () => {
readOnly: next.readOnly,
allowDangerousSql: next.allowDangerousSql,
allowedConnectionIds: next.allowedConnectionIds,
allowedGroupIds: next.allowedGroupIds,
allowedToolNames: next.allowedToolNames,
connectionPolicies: next.connectionPolicies,
groupPolicies: next.groupPolicies,
queryTimeoutSecs: next.queryTimeoutSecs,
});
} catch (error) {