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

+57 -33
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, type McpEnvEntry } from "@/lib/mcp/mcpConfigTemplates";
import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl } from "@/lib/mcp/mcpConfigTemplates";
describe("MCP config templates", () => {
it("builds the standard mcpServers JSON used by Claude, Cursor, TRAE, and Windsurf", () => {
@@ -14,21 +14,8 @@ describe("MCP config templates", () => {
});
});
it("adds DBX MCP env entries to standard JSON configs", () => {
const env: McpEnvEntry[] = [
["DBX_MCP_ALLOW_WRITES", "0"],
["DBX_MCP_ALLOW_DANGEROUS_SQL", "1"],
];
const config = JSON.parse(buildMcpJsonConfig(env));
expect(config.mcpServers.dbx.env).toEqual({
DBX_MCP_ALLOW_WRITES: "0",
DBX_MCP_ALLOW_DANGEROUS_SQL: "1",
});
});
it("builds standard JSON configs with a direct node launch command", () => {
const config = JSON.parse(buildMcpJsonConfig([], { command: "C:\\Program Files\\nodejs\\node.exe", args: ["C:\\Users\\zhiyo\\AppData\\Roaming\\npm\\node_modules\\@dbx-app\\mcp-server\\dist\\index.js"] }));
const config = JSON.parse(buildMcpJsonConfig({ command: "C:\\Program Files\\nodejs\\node.exe", args: ["C:\\Users\\zhiyo\\AppData\\Roaming\\npm\\node_modules\\@dbx-app\\mcp-server\\dist\\index.js"] }));
expect(config).toEqual({
mcpServers: {
@@ -40,24 +27,42 @@ describe("MCP config templates", () => {
});
});
it("builds VS Code MCP config with the servers root", () => {
const config = JSON.parse(buildMcpVsCodeConfig([["DBX_MCP_ALLOW_WRITES", "0"]]));
it("includes Web runtime settings without restoring permission environment variables", () => {
const launch = {
command: "dbx-mcp-server",
env: {
DBX_WEB_URL: "https://dbx.example.com/tools/dbx",
DBX_WEB_PASSWORD: "your-web-login-password",
},
};
expect(JSON.parse(buildMcpJsonConfig(launch))).toEqual({
mcpServers: { dbx: { command: "dbx-mcp-server", env: launch.env } },
});
expect(buildMcpCodexConfig(launch)).toContain('[mcp_servers.dbx.env]\nDBX_WEB_URL = "https://dbx.example.com/tools/dbx"');
expect(JSON.parse(buildMcpOpenCodeConfig(launch)).mcp.dbx.environment).toEqual(launch.env);
expect(buildMcpJsonConfig(launch)).not.toContain("DBX_MCP_ALLOW_WRITES");
});
it("keeps a deployed Web base path in DBX_WEB_URL", () => {
expect(mcpWebBackendUrl("https://dbx.example.com", "/tools/dbx/api")).toBe("https://dbx.example.com/tools/dbx");
});
it("builds VS Code MCP config with the servers root and no policy environment", () => {
const config = JSON.parse(buildMcpVsCodeConfig());
expect(config).toEqual({
servers: {
dbx: {
type: "stdio",
command: "dbx-mcp-server",
env: {
DBX_MCP_ALLOW_WRITES: "0",
},
},
},
});
});
it("builds VS Code config with a direct node launch command", () => {
const config = JSON.parse(buildMcpVsCodeConfig([["DBX_MCP_ALLOW_WRITES", "0"]], { command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] }));
const config = JSON.parse(buildMcpVsCodeConfig({ command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] }));
expect(config).toEqual({
servers: {
@@ -65,40 +70,59 @@ describe("MCP config templates", () => {
type: "stdio",
command: "node",
args: ["C:\\dbx\\mcp\\dist\\index.js"],
env: {
DBX_MCP_ALLOW_WRITES: "0",
},
},
},
});
});
it("builds Codex TOML config with env entries", () => {
expect(buildMcpCodexConfig([["DBX_MCP_ALLOW_WRITES", "0"]])).toBe(["[mcp_servers.dbx]", 'command = "dbx-mcp-server"', "", "[mcp_servers.dbx.env]", 'DBX_MCP_ALLOW_WRITES = "0"'].join("\n"));
it("builds the Cherry Studio stdio configuration", () => {
const config = JSON.parse(
buildMcpCherryStudioConfig({
command: "/opt/homebrew/bin/node",
args: ["/opt/dbx/mcp-server/dist/index.js"],
env: { DBX_WEB_URL: "https://dbx.example.com" },
}),
);
expect(config).toEqual({
mcpServers: {
dbx: {
name: "dbx",
description: "",
baseUrl: "",
command: "/opt/homebrew/bin/node",
args: ["/opt/dbx/mcp-server/dist/index.js"],
env: { DBX_WEB_URL: "https://dbx.example.com" },
isActive: true,
type: "stdio",
},
},
});
});
it("builds Codex TOML config without policy environment", () => {
expect(buildMcpCodexConfig()).toBe(["[mcp_servers.dbx]", 'command = "dbx-mcp-server"'].join("\n"));
});
it("builds Codex TOML config with a direct node launch command", () => {
expect(buildMcpCodexConfig([["DBX_MCP_ALLOW_WRITES", "0"]], { command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] })).toBe(["[mcp_servers.dbx]", 'command = "node"', 'args = ["C:\\\\dbx\\\\mcp\\\\dist\\\\index.js"]', "", "[mcp_servers.dbx.env]", 'DBX_MCP_ALLOW_WRITES = "0"'].join("\n"));
expect(buildMcpCodexConfig({ command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] })).toBe(["[mcp_servers.dbx]", 'command = "node"', 'args = ["C:\\\\dbx\\\\mcp\\\\dist\\\\index.js"]'].join("\n"));
});
it("builds OpenCode config using environment entries", () => {
const config = JSON.parse(buildMcpOpenCodeConfig([["DBX_MCP_ALLOW_DANGEROUS_SQL", "1"]]));
it("builds OpenCode config without policy environment", () => {
const config = JSON.parse(buildMcpOpenCodeConfig());
expect(config).toEqual({
mcp: {
dbx: {
type: "local",
command: ["dbx-mcp-server"],
environment: {
DBX_MCP_ALLOW_DANGEROUS_SQL: "1",
},
},
},
});
});
it("builds OpenCode config with a direct node launch command", () => {
const config = JSON.parse(buildMcpOpenCodeConfig([], { command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] }));
const config = JSON.parse(buildMcpOpenCodeConfig({ command: "node", args: ["C:\\dbx\\mcp\\dist\\index.js"] }));
expect(config).toEqual({
mcp: {
@@ -0,0 +1,117 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { groupMcpScopeConnections, isMcpPolicyMutationBlocked, matchesMcpSearchQuery, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, toggleMcpAllowedConnectionId, updateMcpAllowedConnectionIds } from "@/lib/mcp/mcpPolicySelection";
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
const scopePickerSource = readFileSync(new URL("../../../components/settings/McpConnectionScopePicker.vue", import.meta.url), "utf8");
describe("MCP execution permission selection", () => {
it("maps the persisted policy to the three UI modes", () => {
expect(mcpExecutionModeFromPolicy({ readOnly: true, allowDangerousSql: false })).toBe("read_only");
expect(mcpExecutionModeFromPolicy({ readOnly: false, allowDangerousSql: false })).toBe("safe_write");
expect(mcpExecutionModeFromPolicy({ readOnly: false, allowDangerousSql: true })).toBe("high_risk_write");
});
it("treats read-only as authoritative when legacy state also allows dangerous SQL", () => {
expect(mcpExecutionModeFromPolicy({ readOnly: true, allowDangerousSql: true })).toBe("read_only");
});
it("maps every UI mode to a complete atomic policy update", () => {
expect(mcpPolicyFieldsForExecutionMode("read_only")).toEqual({ readOnly: true, allowDangerousSql: false });
expect(mcpPolicyFieldsForExecutionMode("safe_write")).toEqual({ readOnly: false, allowDangerousSql: false });
expect(mcpPolicyFieldsForExecutionMode("high_risk_write")).toEqual({ readOnly: false, allowDangerousSql: true });
});
it("presents the stable internal modes as three user-facing columns", () => {
expect(MCP_EXECUTION_MODE_COLUMNS.map((column) => column.mode)).toEqual(["read_only", "safe_write", "high_risk_write"]);
});
it("shows the risk-based capability boundary without changing enforcement semantics", () => {
expect(MCP_CAPABILITY_ROWS).toEqual([
{ 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 },
]);
});
});
describe("MCP policy connection selection", () => {
it("turns allow-all into an explicit list when one connection is removed", () => {
expect(toggleMcpAllowedConnectionId(null, ["one", "two", "three"], "two", false)).toEqual(["one", "three"]);
});
it("updates an existing explicit allowlist", () => {
expect(toggleMcpAllowedConnectionId(["one"], ["one", "two"], "two", true)).toEqual(["one", "two"]);
expect(toggleMcpAllowedConnectionId(["one", "two"], ["one", "two"], "one", false)).toEqual(["two"]);
});
it("groups live and unavailable connections without losing source order", () => {
const one = { id: "one", name: "One" };
const two = { id: "two", name: "Two" };
const three = { id: "three", name: "Three" };
expect(groupMcpScopeConnections([one, two, three], ["three", "missing", "one"])).toEqual({
allowed: [one, three],
available: [two],
unavailableAllowedIds: ["missing"],
});
expect(groupMcpScopeConnections([one, two], null)).toEqual({
allowed: [one, two],
available: [],
unavailableAllowedIds: [],
});
});
it("applies batch additions and removals while preserving unavailable IDs", () => {
expect(updateMcpAllowedConnectionIds(["one", "missing"], ["one", "two", "three"], ["two", "three"], true)).toEqual(["one", "missing", "two", "three"]);
expect(updateMcpAllowedConnectionIds(null, ["one", "two", "three"], ["one", "three"], false)).toEqual(["two"]);
});
});
describe("MCP policy settings state", () => {
it("blocks mutations while loading, saving, or displaying a load error", () => {
expect(isMcpPolicyMutationBlocked({ loading: true, saving: false, loadError: "" })).toBe(true);
expect(isMcpPolicyMutationBlocked({ loading: false, saving: true, loadError: "" })).toBe(true);
expect(isMcpPolicyMutationBlocked({ loading: false, saving: false, loadError: "unavailable" })).toBe(true);
expect(isMcpPolicyMutationBlocked({ loading: false, saving: false, loadError: "" })).toBe(false);
});
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('<fieldset :disabled="mcpPolicyControlsDisabled"');
const loadingStart = settingsDialogSource.indexOf("mcpPolicyLoading.value = true;");
const policyLoad = settingsDialogSource.indexOf("await settingsStore.initMcpGlobalPolicy(true);");
const loadingEnd = settingsDialogSource.indexOf("mcpPolicyLoading.value = false;", policyLoad);
expect(loadingStart).toBeGreaterThan(-1);
expect(loadingStart).toBeLessThan(policyLoad);
expect(loadingEnd).toBeGreaterThan(policyLoad);
});
});
describe("MCP connection search", () => {
it("matches case-insensitively across text, numeric fields, and connection IDs", () => {
const values = ["MySQL Local", "mysql", "127.0.0.1", 3306, "app_db", "connection-ABC"];
expect(matchesMcpSearchQuery(" mysql ", values)).toBe(true);
expect(matchesMcpSearchQuery("3306", values)).toBe(true);
expect(matchesMcpSearchQuery("connection-abc", values)).toBe(true);
expect(matchesMcpSearchQuery("postgres", values)).toBe(false);
});
it("treats an empty query as a match and can search unavailable IDs", () => {
expect(matchesMcpSearchQuery(" ", [null, undefined])).toBe(true);
expect(matchesMcpSearchQuery("missing-id", ["missing-id-123", "Previously selected connection (unavailable)"])).toBe(true);
});
it("uses responsive allowed and available panes with an allowed-first compact view", () => {
expect(scopePickerSource).toContain('const compactPane = ref<ScopePane>("allowed")');
expect(scopePickerSource).toContain('data-scope-pane="available"');
expect(scopePickerSource).toContain('data-scope-pane="allowed"');
expect(scopePickerSource).toContain("@container mcp-scope (min-width: 42rem)");
expect(scopePickerSource).toContain("filteredUnavailableAllowedIds");
});
});