diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts index 0b776a2bad9..4954a545f62 100644 --- a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts +++ b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts @@ -89,3 +89,61 @@ describe("filterNativeToolsForMode - disabledTools", () => { expect(resultNames).not.toContain("edit") }) }) + +describe("filterNativeToolsForMode - access_mcp_resource allowlist", () => { + const nativeTools: OpenAI.Chat.ChatCompletionTool[] = [makeTool("read_file"), makeTool("access_mcp_resource")] + + // Minimal McpHub stub exposing only getServers(), which is all the resource + // availability check uses. + function makeMcpHub(servers: Array<{ name: string; resources?: unknown[] }>): any { + return { + getServers: () => servers, + } + } + + it("keeps access_mcp_resource when an allowed server has resources", () => { + const mcpHub = makeMcpHub([{ name: "allowed-server", resources: [{ uri: "res://x" }] }]) + + const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, {}, mcpHub, [ + "allowed-server", + ]) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).toContain("access_mcp_resource") + }) + + it("removes access_mcp_resource when only a disallowed server has resources", () => { + // The server with resources is NOT in the allowlist, so the restricted + // mode must not retain access_mcp_resource. + const mcpHub = makeMcpHub([ + { name: "allowed-server", resources: [] }, + { name: "blocked-server", resources: [{ uri: "res://secret" }] }, + ]) + + const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, {}, mcpHub, [ + "allowed-server", + ]) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).not.toContain("access_mcp_resource") + expect(resultNames).toContain("read_file") + }) + + it("considers all servers when no allowlist is provided (unrestricted mode)", () => { + const mcpHub = makeMcpHub([{ name: "any-server", resources: [{ uri: "res://y" }] }]) + + const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, {}, mcpHub) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).toContain("access_mcp_resource") + }) + + it("removes access_mcp_resource when the allowlist is empty", () => { + const mcpHub = makeMcpHub([{ name: "some-server", resources: [{ uri: "res://z" }] }]) + + const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, {}, mcpHub, []) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).not.toContain("access_mcp_resource") + }) +}) diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index fdd41e7e330..194c7447827 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -220,6 +220,9 @@ export function applyModelToolCustomization( * @param codeIndexManager - Code index manager for codebase_search feature check * @param settings - Additional settings for tool filtering (includes modelInfo for model-specific customization) * @param mcpHub - MCP hub for checking available resources + * @param allowedMcpServers - Optional allowlist of MCP server names for the current mode. When + * provided, the resource-availability check only considers servers in this list, so a mode that + * restricts MCP servers cannot retain `access_mcp_resource` based on resources from disallowed servers. * @returns Filtered array of tools allowed for the mode */ export function filterNativeToolsForMode( @@ -230,6 +233,7 @@ export function filterNativeToolsForMode( codeIndexManager?: CodeIndexManager, settings?: Record, mcpHub?: McpHub, + allowedMcpServers?: string[], ): OpenAI.Chat.ChatCompletionTool[] { // Get mode configuration and all tools for this mode const modeSlug = mode ?? defaultModeSlug @@ -301,8 +305,10 @@ export function filterNativeToolsForMode( } } - // Conditionally exclude access_mcp_resource if MCP is not enabled or there are no resources - if (!mcpHub || !hasAnyMcpResources(mcpHub)) { + // Conditionally exclude access_mcp_resource if MCP is not enabled or there are no resources. + // When the mode restricts MCP servers via allowedMcpServers, only resources from allowed + // servers count — otherwise a restricted mode could still read resources from disallowed servers. + if (!mcpHub || !hasAnyMcpResources(mcpHub, allowedMcpServers)) { allowedToolNames.delete("access_mcp_resource") } @@ -330,10 +336,18 @@ export function filterNativeToolsForMode( } /** - * Helper function to check if any MCP server has resources available + * Helper function to check if any MCP server has resources available. + * + * When `allowedServers` is provided, only servers whose name is in the allowlist are considered. + * This keeps the `access_mcp_resource` availability check consistent with the mode's MCP server + * allowlist so a restricted mode cannot retain the tool based on resources from disallowed servers. */ -function hasAnyMcpResources(mcpHub: McpHub): boolean { - const servers = mcpHub.getServers() +function hasAnyMcpResources(mcpHub: McpHub, allowedServers?: string[]): boolean { + let servers = mcpHub.getServers() + if (allowedServers) { + const allowSet = new Set(allowedServers) + servers = servers.filter((server) => allowSet.has(server.name)) + } return servers.some((server) => server.resources && server.resources.length > 0) } diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index 4a094c2450d..ebbdc050dc9 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -114,7 +114,13 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO supportsImages, }) - // Filter native tools based on mode restrictions. + // Resolve mode config to get allowedMcpServers for MCP server filtering. + const modeConfig = getModeBySlug(mode ?? defaultModeSlug, customModes) + const allowedMcpServers = modeConfig?.allowedMcpServers + + // Filter native tools based on mode restrictions. The allowlist is forwarded so the + // access_mcp_resource availability check only considers resources from allowed servers; + // otherwise a restricted mode could still read resources from disallowed servers. const filteredNativeTools = filterNativeToolsForMode( nativeTools, mode, @@ -123,12 +129,9 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO codeIndexManager, filterSettings, mcpHub, + allowedMcpServers, ) - // Resolve mode config to get allowedMcpServers for MCP server filtering. - const modeConfig = getModeBySlug(mode ?? defaultModeSlug, customModes) - const allowedMcpServers = modeConfig?.allowedMcpServers - // Filter MCP tools based on mode restrictions. const mcpTools = getMcpServerTools(mcpHub, allowedMcpServers) const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments) diff --git a/webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.tsx b/webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.tsx index 470e8c6e611..cc6d089a1ad 100644 --- a/webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.tsx +++ b/webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.tsx @@ -1,8 +1,14 @@ import React from "react" export const VSCodeCheckbox = ({ children, onChange, checked, "data-testid": dataTestId, ...props }: any) => ( -