diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 732284e56f..46c2932421 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -181,7 +181,7 @@ import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, import { DEFAULT_SQL_VARIABLE_SYNTAX_TOGGLES, normalizeSqlVariableSyntaxOverrides, SQL_VARIABLE_SYNTAX_DATABASE_TYPES, SQL_VARIABLE_SYNTAX_KEYS, SQL_VARIABLE_SYNTAX_TOKENS, type SqlVariableSyntaxOverrides, type SqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; import { buildMcpCherryStudioConfig, buildMcpCodexConfig, buildMcpDeepSeekHarnessConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpPiConfig, buildMcpQoderConfig, buildMcpTraeConfig, buildMcpVsCodeConfig, mcpWebBackendUrl, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates"; import { beginMcpStatusRequest, mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus"; -import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection"; +import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, MCP_TOOL_OPTIONS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, toggleMcpAllowedToolName, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection"; import { isMacOS, isWindows } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; import { useToast } from "@/composables/useToast"; @@ -2682,25 +2682,7 @@ function onMcpResourceScopeChange(scope: { allowedGroupIds: string[]; allowedCon type McpConnectionExecutionMode = "read_only" | "safe_write" | "high_risk_write"; -const mcpToolOptions = [ - { name: "dbx_list_connections", labelKey: "settings.mcpToolListConnections" }, - { name: "dbx_list_databases", labelKey: "settings.mcpToolListDatabases" }, - { name: "dbx_list_tables", labelKey: "settings.mcpToolListTables" }, - { name: "dbx_describe_table", labelKey: "settings.mcpToolDescribeTable" }, - { name: "dbx_list_routines", labelKey: "settings.mcpToolListRoutines" }, - { name: "dbx_get_routine_source", labelKey: "settings.mcpToolGetRoutineSource" }, - { name: "dbx_get_schema_context", labelKey: "settings.mcpToolGetSchemaContext" }, - { name: "dbx_execute_query", labelKey: "settings.mcpToolExecuteQuery" }, - { name: "dbx_open_session", labelKey: "settings.mcpToolOpenSession" }, - { name: "dbx_close_session", labelKey: "settings.mcpToolCloseSession" }, - { name: "dbx_execute_redis_command", labelKey: "settings.mcpToolExecuteRedisCommand" }, - { name: "dbx_send_message", labelKey: "settings.mcpToolSendMessage" }, - { name: "dbx_add_connection", labelKey: "settings.mcpToolAddConnection" }, - { name: "dbx_duplicate_connection", labelKey: "settings.mcpToolDuplicateConnection" }, - { name: "dbx_remove_connection", labelKey: "settings.mcpToolRemoveConnection" }, - { name: "dbx_open_table", labelKey: "settings.mcpToolOpenTable" }, - { name: "dbx_execute_and_show", labelKey: "settings.mcpToolExecuteAndShow" }, -] as const; +const mcpToolOptions = MCP_TOOL_OPTIONS; const mcpAllowedToolNames = computed(() => settingsStore.mcpGlobalPolicy.allowedToolNames); @@ -2709,9 +2691,7 @@ function mcpToolAllowed(name: string): boolean { } function onMcpToolAllowedChange(name: string, allowed: boolean) { - const current = mcpAllowedToolNames.value ?? mcpToolOptions.map((tool) => tool.name); - const next = allowed ? [...new Set([...current, name])] : current.filter((tool) => tool !== name); - void saveMcpPolicy({ allowedToolNames: next }); + void saveMcpPolicy({ allowedToolNames: toggleMcpAllowedToolName(mcpAllowedToolNames.value, name, allowed) }); } const mcpConnectionPolicyConnections = computed(() => { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 83f9b2b8e4..f890a2f03f 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -7513,6 +7513,7 @@ export default { mcpToolGetRoutineSource: "Routine source", mcpToolGetSchemaContext: "Schema context", mcpToolExecuteQuery: "Execute SQL / Mongo commands", + mcpToolExecuteBatch: "Execute SQL batch", mcpToolOpenSession: "Open query session", mcpToolCloseSession: "Close query session", mcpToolExecuteRedisCommand: "Execute Redis command", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index c52d4eb7e1..937560ed66 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -7039,6 +7039,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "Código de rutina", mcpToolGetSchemaContext: "Contexto de Schema", mcpToolExecuteQuery: "Ejecutar SQL / comandos Mongo", + mcpToolExecuteBatch: "Ejecutar lote SQL", mcpToolOpenSession: "Abrir sesión de consulta", mcpToolCloseSession: "Cerrar sesión de consulta", mcpToolExecuteRedisCommand: "Ejecutar comando Redis", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index ec1a899749..07c01f83c1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -7039,6 +7039,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "Sorgente routine", mcpToolGetSchemaContext: "Contesto Schema", mcpToolExecuteQuery: "Esegui SQL / comandi Mongo", + mcpToolExecuteBatch: "Esegui batch SQL", mcpToolOpenSession: "Apri sessione di query", mcpToolCloseSession: "Chiudi sessione di query", mcpToolExecuteRedisCommand: "Esegui comando Redis", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 4180c7f47a..d6fc531671 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -7046,6 +7046,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "ルーチンソース", mcpToolGetSchemaContext: "Schema コンテキスト", mcpToolExecuteQuery: "SQL / Mongo コマンドを実行", + mcpToolExecuteBatch: "SQL バッチを実行", mcpToolOpenSession: "クエリセッションを開く", mcpToolCloseSession: "クエリセッションを閉じる", mcpToolExecuteRedisCommand: "Redis コマンドを実行", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 7062ecbcbe..d48df0e12e 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -6792,6 +6792,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "루틴 소스", mcpToolGetSchemaContext: "Schema 컨텍스트", mcpToolExecuteQuery: "SQL / Mongo 명령 실행", + mcpToolExecuteBatch: "SQL 배치 실행", mcpToolOpenSession: "쿼리 세션 열기", mcpToolCloseSession: "쿼리 세션 닫기", mcpToolExecuteRedisCommand: "Redis 명령 실행", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index ac183a7cb2..9cdddca27e 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -7041,6 +7041,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "Código da rotina", mcpToolGetSchemaContext: "Contexto do Schema", mcpToolExecuteQuery: "Executar SQL / comandos Mongo", + mcpToolExecuteBatch: "Executar lote SQL", mcpToolOpenSession: "Abrir sessão de consulta", mcpToolCloseSession: "Fechar sessão de consulta", mcpToolExecuteRedisCommand: "Executar comando Redis", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 5c8104ad4b..6f21ceb462 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7398,6 +7398,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "Yordam kaynağı", mcpToolGetSchemaContext: "Şema bağlamı", mcpToolExecuteQuery: "SQL / Mongo komutlarını çalıştır", + mcpToolExecuteBatch: "SQL toplu işlemini çalıştır", mcpToolOpenSession: "Sorgu oturumu aç", mcpToolCloseSession: "Sorgu oturumunu kapat", mcpToolExecuteRedisCommand: "Redis komutunu çalıştır", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index d9dcae25d0..3c03af7cd5 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -7498,6 +7498,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "存储过程源码", mcpToolGetSchemaContext: "Schema 上下文", mcpToolExecuteQuery: "执行 SQL / Mongo 命令", + mcpToolExecuteBatch: "批量执行 SQL", mcpToolOpenSession: "打开查询会话", mcpToolCloseSession: "关闭查询会话", mcpToolExecuteRedisCommand: "执行 Redis 命令", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 03cfbd8cc9..8cd9daaae0 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -6400,6 +6400,7 @@ export default withEnglishFallback({ mcpToolGetRoutineSource: "預存程序原始碼", mcpToolGetSchemaContext: "Schema 上下文", mcpToolExecuteQuery: "執行 SQL / Mongo 命令", + mcpToolExecuteBatch: "批次執行 SQL", mcpToolOpenSession: "開啟查詢工作階段", mcpToolCloseSession: "關閉查詢工作階段", mcpToolExecuteRedisCommand: "執行 Redis 命令", diff --git a/apps/desktop/src/lib/__tests__/mcp/mcpPolicySelection.spec.ts b/apps/desktop/src/lib/__tests__/mcp/mcpPolicySelection.spec.ts index acd244d9ac..ad0769dd18 100644 --- a/apps/desktop/src/lib/__tests__/mcp/mcpPolicySelection.spec.ts +++ b/apps/desktop/src/lib/__tests__/mcp/mcpPolicySelection.spec.ts @@ -1,10 +1,23 @@ 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"; +import { + groupMcpScopeConnections, + isMcpPolicyMutationBlocked, + matchesMcpSearchQuery, + MCP_CAPABILITY_ROWS, + MCP_EXECUTION_MODE_COLUMNS, + MCP_TOOL_OPTIONS, + mcpExecutionModeFromPolicy, + mcpPolicyFieldsForExecutionMode, + toggleMcpAllowedConnectionId, + toggleMcpAllowedToolName, + 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"); +const mcpServerSource = readFileSync(new URL("../../../../../../crates/dbx-mcp/src/server.rs", import.meta.url), "utf8"); describe("MCP execution permission selection", () => { it("maps the persisted policy to the three UI modes", () => { @@ -70,6 +83,29 @@ describe("MCP policy connection selection", () => { }); }); +describe("MCP tool permission selection", () => { + it("lists every tool registered by the MCP server", () => { + const registeredToolNames = [...mcpServerSource.matchAll(/name\s*=\s*"(dbx_[^"]+)"/g)].map((match) => match[1]).sort(); + + expect(MCP_TOOL_OPTIONS.map((tool) => tool.name).sort()).toEqual(registeredToolNames); + }); + + it("keeps batch execution allowed when allow-all becomes an explicit allowlist", () => { + const next = toggleMcpAllowedToolName(null, "dbx_send_message", false); + + expect(next).toContain("dbx_execute_batch"); + expect(next).not.toContain("dbx_send_message"); + }); + + it("lets batch execution be enabled and disabled independently", () => { + expect(toggleMcpAllowedToolName(["dbx_execute_query"], "dbx_execute_batch", true)).toEqual(["dbx_execute_query", "dbx_execute_batch"]); + expect(toggleMcpAllowedToolName(["dbx_execute_query", "dbx_execute_batch"], "dbx_execute_batch", false)).toEqual(["dbx_execute_query"]); + expect(MCP_TOOL_OPTIONS.find((tool) => tool.name === "dbx_execute_batch")?.labelKey).toBe("settings.mcpToolExecuteBatch"); + expect(settingsDialogSource).toContain("const mcpToolOptions = MCP_TOOL_OPTIONS;"); + expect(settingsDialogSource).toContain("toggleMcpAllowedToolName(mcpAllowedToolNames.value, name, allowed)"); + }); +}); + 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); diff --git a/apps/desktop/src/lib/mcp/mcpPolicySelection.ts b/apps/desktop/src/lib/mcp/mcpPolicySelection.ts index 73ee42a369..c99590e74f 100644 --- a/apps/desktop/src/lib/mcp/mcpPolicySelection.ts +++ b/apps/desktop/src/lib/mcp/mcpPolicySelection.ts @@ -21,6 +21,27 @@ export const MCP_CAPABILITY_ROWS: readonly McpCapabilityRow[] = [ { labelKey: "settings.mcpCapabilityConnectionManagement", read_only: false, safe_write: true, high_risk_write: true }, ]; +export const MCP_TOOL_OPTIONS = [ + { name: "dbx_list_connections", labelKey: "settings.mcpToolListConnections" }, + { name: "dbx_list_databases", labelKey: "settings.mcpToolListDatabases" }, + { name: "dbx_list_tables", labelKey: "settings.mcpToolListTables" }, + { name: "dbx_describe_table", labelKey: "settings.mcpToolDescribeTable" }, + { name: "dbx_list_routines", labelKey: "settings.mcpToolListRoutines" }, + { name: "dbx_get_routine_source", labelKey: "settings.mcpToolGetRoutineSource" }, + { name: "dbx_get_schema_context", labelKey: "settings.mcpToolGetSchemaContext" }, + { name: "dbx_execute_query", labelKey: "settings.mcpToolExecuteQuery" }, + { name: "dbx_execute_batch", labelKey: "settings.mcpToolExecuteBatch" }, + { name: "dbx_open_session", labelKey: "settings.mcpToolOpenSession" }, + { name: "dbx_close_session", labelKey: "settings.mcpToolCloseSession" }, + { name: "dbx_execute_redis_command", labelKey: "settings.mcpToolExecuteRedisCommand" }, + { name: "dbx_send_message", labelKey: "settings.mcpToolSendMessage" }, + { name: "dbx_add_connection", labelKey: "settings.mcpToolAddConnection" }, + { name: "dbx_duplicate_connection", labelKey: "settings.mcpToolDuplicateConnection" }, + { name: "dbx_remove_connection", labelKey: "settings.mcpToolRemoveConnection" }, + { name: "dbx_open_table", labelKey: "settings.mcpToolOpenTable" }, + { name: "dbx_execute_and_show", labelKey: "settings.mcpToolExecuteAndShow" }, +] as const; + export interface McpExecutionPolicyFields { readOnly: boolean; allowDangerousSql: boolean; @@ -90,6 +111,13 @@ export function toggleMcpAllowedConnectionId(current: readonly string[] | null, return updateMcpAllowedConnectionIds(current, availableConnectionIds, [connectionId], allowed); } +export function toggleMcpAllowedToolName(current: readonly string[] | null, toolName: string, allowed: boolean): string[] { + const selected = new Set(current === null ? MCP_TOOL_OPTIONS.map((tool) => tool.name) : current); + if (allowed) selected.add(toolName); + else selected.delete(toolName); + return [...selected]; +} + 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) {