feat(mcp): centralize connection access policies
This commit is contained in:
56 files changed
+6393
-617
No files matched your search
@@ -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];
|
||||
}
|
||||
Reference in new issue
Block a user