feat(mcp): centralize connection access policies

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-20 22:47:40 +08:00
1 parent 29f493f7da
commit d642d01eac
56 files changed
+6393 -617

No files matched your search

+33 -26
View File
@@ -1,18 +1,13 @@
export type McpEnvEntry = readonly [key: string, value: string];
export interface McpLaunchConfig {
command: string;
args?: readonly string[];
env?: Readonly<Record<string, string>>;
}
const DEFAULT_MCP_LAUNCH_CONFIG: McpLaunchConfig = {
command: "dbx-mcp-server",
};
function envObject(envEntries: readonly McpEnvEntry[]): Record<string, string> {
return Object.fromEntries(envEntries);
}
function launchConfig(config?: McpLaunchConfig): McpLaunchConfig {
return config ?? DEFAULT_MCP_LAUNCH_CONFIG;
}
@@ -23,6 +18,9 @@ function withLaunchConfig(dbx: Record<string, unknown>, config?: McpLaunchConfig
if (launch.args && launch.args.length > 0) {
dbx.args = [...launch.args];
}
if (launch.env && Object.keys(launch.env).length > 0) {
dbx.env = { ...launch.env };
}
return dbx;
}
@@ -30,43 +28,53 @@ function tomlStringArray(values: readonly string[]): string {
return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
}
export function buildMcpJsonConfig(envEntries: readonly McpEnvEntry[] = [], config?: McpLaunchConfig): string {
export function mcpWebBackendUrl(origin: string, apiPath: string): string {
return new URL(apiPath, origin).toString().replace(/\/api\/?$/, "");
}
export function buildMcpJsonConfig(config?: McpLaunchConfig): string {
const dbx: Record<string, unknown> = {
...withLaunchConfig({}, config),
};
if (envEntries.length > 0) {
dbx.env = envObject(envEntries);
}
return JSON.stringify({ mcpServers: { dbx } }, null, 2);
}
export function buildMcpVsCodeConfig(envEntries: readonly McpEnvEntry[] = [], config?: McpLaunchConfig): string {
export function buildMcpVsCodeConfig(config?: McpLaunchConfig): string {
const dbx: Record<string, unknown> = {
type: "stdio",
...withLaunchConfig({}, config),
};
if (envEntries.length > 0) {
dbx.env = envObject(envEntries);
}
return JSON.stringify({ servers: { dbx } }, null, 2);
}
export function buildMcpCodexConfig(envEntries: readonly McpEnvEntry[] = [], config?: McpLaunchConfig): string {
export function buildMcpCherryStudioConfig(config?: McpLaunchConfig): string {
const launch = launchConfig(config);
const dbx: Record<string, unknown> = {
name: "dbx",
description: "",
baseUrl: "",
command: launch.command,
args: [...(launch.args ?? [])],
env: { ...launch.env },
isActive: true,
type: "stdio",
};
return JSON.stringify({ mcpServers: { dbx } }, null, 2);
}
export function buildMcpCodexConfig(config?: McpLaunchConfig): string {
const launch = launchConfig(config);
const lines = ["[mcp_servers.dbx]", `command = ${JSON.stringify(launch.command)}`];
if (launch.args && launch.args.length > 0) {
lines.push(`args = ${tomlStringArray(launch.args)}`);
}
if (envEntries.length > 0) {
lines.push("");
lines.push("[mcp_servers.dbx.env]");
for (const [key, value] of envEntries) {
if (launch.env && Object.keys(launch.env).length > 0) {
lines.push("", "[mcp_servers.dbx.env]");
for (const [key, value] of Object.entries(launch.env)) {
lines.push(`${key} = ${JSON.stringify(value)}`);
}
}
@@ -74,15 +82,14 @@ export function buildMcpCodexConfig(envEntries: readonly McpEnvEntry[] = [], con
return lines.join("\n");
}
export function buildMcpOpenCodeConfig(envEntries: readonly McpEnvEntry[] = [], config?: McpLaunchConfig): string {
export function buildMcpOpenCodeConfig(config?: McpLaunchConfig): string {
const launch = launchConfig(config);
const dbx: Record<string, unknown> = {
type: "local",
command: [launch.command, ...(launch.args ?? [])],
};
if (envEntries.length > 0) {
dbx.environment = envObject(envEntries);
if (launch.env && Object.keys(launch.env).length > 0) {
dbx.environment = { ...launch.env };
}
return JSON.stringify({ mcp: { dbx } }, null, 2);
@@ -0,0 +1,100 @@
export type McpExecutionMode = "read_only" | "safe_write" | "high_risk_write";
export interface McpCapabilityRow {
labelKey: string;
read_only: boolean;
safe_write: boolean;
high_risk_write: boolean;
}
export const MCP_EXECUTION_MODE_COLUMNS: ReadonlyArray<{ mode: McpExecutionMode; labelKey: string }> = [
{ mode: "read_only", labelKey: "settings.mcpExecutionModeReadOnly" },
{ mode: "safe_write", labelKey: "settings.mcpExecutionModeSafeWrite" },
{ mode: "high_risk_write", labelKey: "settings.mcpExecutionModeHighRiskWrite" },
];
export const MCP_CAPABILITY_ROWS: readonly McpCapabilityRow[] = [
{ labelKey: "settings.mcpCapabilityRead", read_only: true, safe_write: true, high_risk_write: true },
{ labelKey: "settings.mcpCapabilityScopedMutation", read_only: false, safe_write: true, high_risk_write: true },
{ labelKey: "settings.mcpCapabilityBroadMutation", read_only: false, safe_write: false, high_risk_write: true },
{ labelKey: "settings.mcpCapabilitySchemaAdmin", read_only: false, safe_write: false, high_risk_write: true },
{ labelKey: "settings.mcpCapabilityConnectionManagement", read_only: false, safe_write: true, high_risk_write: true },
];
export interface McpExecutionPolicyFields {
readOnly: boolean;
allowDangerousSql: boolean;
}
export interface McpPolicyMutationState {
loading: boolean;
saving: boolean;
loadError: string;
}
export interface McpScopeConnectionLike {
id: string;
}
export interface McpScopeConnectionGroups<T extends McpScopeConnectionLike> {
allowed: T[];
available: T[];
unavailableAllowedIds: string[];
}
export function isMcpPolicyMutationBlocked(state: McpPolicyMutationState): boolean {
return state.loading || state.saving || state.loadError.length > 0;
}
export function matchesMcpSearchQuery(rawQuery: string, values: readonly unknown[]): boolean {
const query = rawQuery.trim().toLocaleLowerCase();
if (!query) return true;
return values.some((value) => value !== null && value !== undefined && String(value).toLocaleLowerCase().includes(query));
}
export function groupMcpScopeConnections<T extends McpScopeConnectionLike>(connections: readonly T[], allowedConnectionIds: readonly string[] | null): McpScopeConnectionGroups<T> {
if (allowedConnectionIds === null) {
return {
allowed: [...connections],
available: [],
unavailableAllowedIds: [],
};
}
const allowedIds = new Set(allowedConnectionIds);
const connectionIds = new Set(connections.map((connection) => connection.id));
return {
allowed: connections.filter((connection) => allowedIds.has(connection.id)),
available: connections.filter((connection) => !allowedIds.has(connection.id)),
unavailableAllowedIds: allowedConnectionIds.filter((id) => !connectionIds.has(id)),
};
}
export function mcpExecutionModeFromPolicy(policy: McpExecutionPolicyFields): McpExecutionMode {
if (policy.readOnly) return "read_only";
return policy.allowDangerousSql ? "high_risk_write" : "safe_write";
}
export function mcpPolicyFieldsForExecutionMode(mode: McpExecutionMode): McpExecutionPolicyFields {
switch (mode) {
case "read_only":
return { readOnly: true, allowDangerousSql: false };
case "safe_write":
return { readOnly: false, allowDangerousSql: false };
case "high_risk_write":
return { readOnly: false, allowDangerousSql: true };
}
}
export function toggleMcpAllowedConnectionId(current: readonly string[] | null, availableConnectionIds: readonly string[], connectionId: string, allowed: boolean): string[] {
return updateMcpAllowedConnectionIds(current, availableConnectionIds, [connectionId], allowed);
}
export function updateMcpAllowedConnectionIds(current: readonly string[] | null, availableConnectionIds: readonly string[], connectionIds: readonly string[], allowed: boolean): string[] {
const selected = new Set(current === null ? availableConnectionIds : current);
for (const connectionId of connectionIds) {
if (allowed) selected.add(connectionId);
else selected.delete(connectionId);
}
return [...selected];
}