From 88d84beede8a352ff9733240f543b2539ec6520f Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Sat, 12 Sep 2026 06:43:26 -0700 Subject: [PATCH] fix(desktop): treat an explicit empty MCP include list as block-all `isToolEnabled` used `include?.length`, so `tools.include: []` (the block-all whitelist the runtime honours) rendered every tool as enabled, and toggling the last include entry off deleted the key, silently flipping the server back to "all tools". Keep the empty list. Salvaged from #52874 (desktop portion) by @Bartok9. Part of #12865. --- apps/desktop/src/lib/mcp-tool-filter.test.ts | 13 ++++++++++++ apps/desktop/src/lib/mcp-tool-filter.ts | 22 +++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/lib/mcp-tool-filter.test.ts b/apps/desktop/src/lib/mcp-tool-filter.test.ts index ade59df5e5..4f11840d07 100644 --- a/apps/desktop/src/lib/mcp-tool-filter.test.ts +++ b/apps/desktop/src/lib/mcp-tool-filter.test.ts @@ -31,6 +31,13 @@ describe('isToolEnabled', () => { expect(isToolEnabled(server, 'a')).toBe(true) expect(isToolEnabled(server, 'b')).toBe(false) }) + + it('empty include blocks every tool (block-all, not all-on)', () => { + const server = { tools: { include: [] as string[] } } + expect(isToolEnabled(server, 'a')).toBe(false) + expect(isToolEnabled(server, 'b')).toBe(false) + expect(countEnabledTools(server, ['a', 'b', 'c'])).toBe(0) + }) }) describe('toggleToolInServer', () => { @@ -49,6 +56,12 @@ describe('toggleToolInServer', () => { expect(next.tools).toEqual({ include: ['b'] }) }) + it('retains empty include when last include tool is toggled off', () => { + const next = toggleToolInServer({ tools: { include: ['a'] } }, 'a') + expect(next.tools).toEqual({ include: [] }) + expect(isToolEnabled(next, 'a')).toBe(false) + }) + it('respects include mode: re-enabling adds back to include', () => { const next = toggleToolInServer({ tools: { include: ['b'] } }, 'a') expect(next.tools).toEqual({ include: ['b', 'a'] }) diff --git a/apps/desktop/src/lib/mcp-tool-filter.ts b/apps/desktop/src/lib/mcp-tool-filter.ts index 8653c32e03..a0fabf4643 100644 --- a/apps/desktop/src/lib/mcp-tool-filter.ts +++ b/apps/desktop/src/lib/mcp-tool-filter.ts @@ -28,22 +28,30 @@ export function readToolsFilter(server: ServerConfig | null | undefined): McpToo export function isToolEnabled(server: ServerConfig | null | undefined, name: string): boolean { const { exclude, include } = readToolsFilter(server) - return include?.length ? include.includes(name) : !exclude?.includes(name) + // An explicit `include` (even []) is a whitelist — the runtime registers nothing for `[]` + // (tools/mcp_tool_registration.py), so the desktop must not show every tool as enabled (#12865). + if (include !== undefined) { + return include.includes(name) + } + + return !exclude?.includes(name) } -// Toggle one tool, preserving the config's mode (include if present, else an -// exclude denylist). Empty lists — and an emptied `tools` — are dropped. +// Toggle one tool, preserving the config's mode (include if the key is present, even empty, else +// an exclude denylist). An emptied exclude is dropped; an emptied include is kept (block-all). export function toggleToolInServer(server: ServerConfig, name: string): ServerConfig { const { exclude, include } = readToolsFilter(server) - const key = include?.length ? 'include' : 'exclude' + const key = include !== undefined ? 'include' : 'exclude' const current = (key === 'include' ? include : exclude) ?? [] const names = current.includes(name) ? current.filter(n => n !== name) : [...current, name] const tools = { ...toolsObject(server) } - if (names.length) { - tools[key] = names + if (key === 'include') { + tools.include = names + } else if (names.length) { + tools.exclude = names } else { - delete tools[key] + delete tools.exclude } const next = { ...server }