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 }