From ee8309975d8d1f09875f062c063a37ddbb2125ad Mon Sep 17 00:00:00 2001 From: luotianyiismywife Date: Sat, 5 Sep 2026 06:13:21 +0800 Subject: [PATCH] Fix ask mode MCP tool availability after server discovery When the user references an MCP server in ask mode, the toolReferences in the request are a snapshot taken when the request was parsed. If the MCP server is still discovering its tools at that point, the snapshot contains only the tools registered so far, and any tools discovered later are reported as "disabled by the user" when the model tries to call them (they are not in availableTools). Include all currently registered MCP tools in the ask agent tool filter when the user referenced an MCP server, so availability reflects the live registration state rather than the possibly-stale snapshot. Fixes #334569 --- .../extension/intents/node/askAgentIntent.ts | 38 ++++++++-- .../intents/test/node/askAgentIntent.spec.ts | 73 +++++++++++++++++++ 2 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts diff --git a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts index 9d675b67529a95..cb225cac8bdfcb 100644 --- a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts @@ -44,14 +44,40 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const lookForTags = new Set(['vscode_codesearch']); const endpointProvider = accessor.get(IEndpointProvider); const model = await endpointProvider.getChatEndpoint(request); - - // Special case... - // Since AskAgent currently has no tool picker, have to duplicate the toolReference logic here. - // When it's no longer experimental, it should be a custom mode, have a tool picker, etc. - // And must return boolean to avoid falling back on other logic that we don't want, like the `extension_installed_by_tool` check. - return toolsService.getEnabledTools(request, model, tool => tool.tags.some(tag => lookForTags.has(tag)) || request.toolReferences.some(ref => ref.name === tool.name)); + return toolsService.getEnabledTools(request, model, tool => askAgentToolFilter(tool, request)); }); +/** + * Filters tools for the ask agent. Since AskAgent currently has no tool picker, + * we duplicate the toolReference logic here. When it's no longer experimental, + * it should be a custom mode, have a tool picker, etc. + * + * MCP tools are registered dynamically as the MCP server connects and discovers + * its tools. The `toolReferences` in the request are a snapshot taken when the + * request was parsed, which can happen before the MCP server finished + * discovering all of its tools. When the user referenced an MCP server (any of + * its tools appear in `toolReferences`), include all currently registered MCP + * tools so that availability reflects the live registration state rather than + * the possibly-stale snapshot. See #334569. + * + * Must return boolean to avoid falling back on other logic that we don't want, + * like the `extension_installed_by_tool` check. + */ +export function askAgentToolFilter(tool: vscode.LanguageModelToolInformation, request: vscode.ChatRequest): boolean { + const lookForTags = new Set(['vscode_codesearch']); + if (tool.tags.some(tag => lookForTags.has(tag)) || request.toolReferences.some(ref => ref.name === tool.name)) { + return true; + } + if (tool.tags.includes('mcp')) { + // The user referenced an MCP server. MCP tools are registered with the + // `mcp_` prefix (see McpToolName.Prefix), so check the reference names + // rather than looking up the tool, which may not be registered yet when + // the request snapshot was taken. + return request.toolReferences.some(ref => ref.name.startsWith('mcp_')); + } + return false; +} + export class AskAgentIntent implements IIntent { static readonly ID = Intent.AskAgent; diff --git a/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts b/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts new file mode 100644 index 00000000000000..a3a58776d53178 --- /dev/null +++ b/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import type { ChatRequest, LanguageModelToolInformation } from 'vscode'; +import { askAgentToolFilter } from '../../node/askAgentIntent'; + +function makeTool(name: string, tags: string[] = []): LanguageModelToolInformation { + return { + name, + description: `description for ${name}`, + inputSchema: { type: 'object' }, + tags, + source: undefined, + }; +} + +function makeRequest(toolReferences: { name: string }[]): ChatRequest { + return { + requestId: 'request-id', + message: 'test', + agentId: 'copilot-chat', + prompt: 'test', + references: [], + model: undefined, + toolReferences, + tools: [], + history: [], + }; +} + +describe('askAgentToolFilter', () => { + it('includes tools with the codesearch tag', () => { + const tool = makeTool('vscode_search', ['vscode_codesearch']); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('includes tools referenced in the request', () => { + const tool = makeTool('vscode_read_file', []); + const request = makeRequest([{ name: 'vscode_read_file' }]); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('excludes unreferenced non-MCP tools', () => { + const tool = makeTool('vscode_edit_file', []); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); + + it('includes MCP tools when the user referenced an MCP server tool', () => { + // The request snapshot was taken while the MCP server was still + // discovering tools, so it only contains one of the server's tools. + const request = makeRequest([{ name: 'mcp_fire_take_snapshot' }]); + // Tools registered *after* the snapshot are now available too. + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('excludes MCP tools when the user did not reference an MCP server', () => { + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + const request = makeRequest([{ name: 'vscode_read_file' }]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); + + it('excludes MCP tools when the request has no tool references at all', () => { + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); +});