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.
This commit is contained in:
Bartok9
2026-09-12 06:43:26 -07:00
committed by Teknium
parent 088d292d36
commit 88d84beede
2 changed files with 28 additions and 7 deletions

View File

@@ -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'] })

View File

@@ -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 }