From 8eaafbf4220a3cb099d7bd99c8d04dca7062236f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 7 Aug 2026 09:53:13 -0700 Subject: [PATCH] agentHost: exclude Copilot Chat GitHub MCP sync (#329623) Copilot CLI agent hosts already configure the SDK's built-in GitHub MCP server with form deferral disabled. Avoid syncing the extension-contributed duplicate so it cannot override that behavior, while retaining user servers and non-Copilot agent-host support.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/agentHostLocalCustomizations.ts | 27 ++++-- .../resolveCustomizationRefs.test.ts | 84 ++++++++++++++++++- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts index 460ed39200b..8f90f42e05d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts @@ -7,8 +7,10 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { Iterable } from '../../../../../../base/common/iterator.js'; import { basename, isEqualOrParent } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { parseRemoteAgentHostHarness } from '../../../../../../platform/agentHost/common/agentHostSessionType.js'; import { type AgentCustomization, CustomizationType, type URI as ProtocolURI } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { customizationId, type ClientPluginCustomization } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; import { IMcpServerConfiguration, McpServerType } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js'; import { AICustomizationSource, AICustomizationSources } from '../../../common/aiCustomizationWorkspaceService.js'; import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; @@ -17,7 +19,7 @@ import { type ICustomizationSyncProvider } from '../../../common/customizationHa import { IAgentPlugin, IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; import { isContributionEnabled } from '../../../common/enablement.js'; import { MCP_PLUGIN_COLLECTION_ID_PREFIX } from '../../../../mcp/common/discovery/pluginMcpDiscovery.js'; -import { IMcpService, McpCollectionDefinition, McpServerLaunch, McpServerTransportType } from '../../../../mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, IMcpService, McpCollectionDefinition, McpServerLaunch, McpServerTransportType } from '../../../../mcp/common/mcpTypes.js'; import { IConfigurationResolverService } from '../../../../../services/configurationResolver/common/configurationResolver.js'; import { ConfigurationResolverExpression } from '../../../../../services/configurationResolver/common/configurationResolverExpression.js'; import { IWorkspaceFolderData } from '../../../../../../platform/workspace/common/workspace.js'; @@ -27,6 +29,13 @@ import { IFileService } from '../../../../../../platform/files/common/files.js'; import { isDefined } from '../../../../../../base/common/types.js'; import { PromptFileParser } from '../../../common/promptSyntax/promptFileParser.js'; +const COPILOT_CHAT_EXTENSION_ID = 'github.copilot-chat'; +const COPILOT_CHAT_GITHUB_MCP_COLLECTION_ID = extensionPrefixedIdentifier(new ExtensionIdentifier(COPILOT_CHAT_EXTENSION_ID), 'github'); + +function hasBuiltInGitHubMcpServer(sessionType: string): boolean { + return sessionType === AGENT_HOST_COPILOT_CLI_SESSION_TYPE || parseRemoteAgentHostHarness(sessionType) === 'copilotcli'; +} + /** * Prompt types that participate in auto-sync to an agent host harness. * @@ -289,7 +298,9 @@ export function shouldSyncWorkspaceDotMcp(sessionType: string, workspaceFolderCo * exception is `.vscode/mcp.json`, which the agent host does not discover * (despite what the SDK's `enableConfigDiscovery` docs imply) — those are * synced, but only when their config can be resolved without requiring user - * interaction. + * interaction. For Copilot CLI agent-host sessions, the Copilot Chat + * extension's GitHub MCP provider is excluded because the SDK supplies its own + * built-in GitHub server. * * When {@link includeWorkspaceDotMcp} is `true` (multi-root Copilot Agent Host * gate), folder-root `.mcp.json` servers are additionally synced so servers @@ -299,7 +310,7 @@ export function shouldSyncWorkspaceDotMcp(sessionType: string, workspaceFolderCo * are passed as-is: `.mcp.json` supports no `${...}` variables and already * carries an explicit absolute `cwd`. */ -export async function collectNonPluginMcpServers(mcpService: IMcpService, configurationResolverService: IConfigurationResolverService, includeWorkspaceDotMcp: boolean): Promise { +export async function collectNonPluginMcpServers(mcpService: IMcpService, configurationResolverService: IConfigurationResolverService, sessionType: string, includeWorkspaceDotMcp: boolean): Promise { const result: ISyncableMcpServer[] = []; for (const server of mcpService.servers.get()) { if (server.collection.id.startsWith(MCP_PLUGIN_COLLECTION_ID_PREFIX)) { @@ -314,11 +325,17 @@ export async function collectNonPluginMcpServers(mcpService: IMcpService, config if (!launch) { continue; } + const collection = definitions.collection; + if (hasBuiltInGitHubMcpServer(sessionType) + && collection?.id === COPILOT_CHAT_GITHUB_MCP_COLLECTION_ID + && collection.source instanceof ExtensionIdentifier + && ExtensionIdentifier.equals(collection.source, COPILOT_CHAT_EXTENSION_ID)) { + continue; + } let configuration = launchToMcpServerConfiguration(launch); if (!configuration) { continue; } - const collection = definitions.collection; if (collection && McpCollectionDefinition.isWorkspaceDiscovered(collection)) { if (McpCollectionDefinition.isVscodeMcpJson(collection)) { const resolved = await resolveConfigurationForSync(configurationResolverService, definition.variableReplacement?.folder, configuration); @@ -439,7 +456,7 @@ export async function resolveCustomizationRefs( } const refs: Promise[] = [...pluginRefs.values()]; - const mcpServers = await collectNonPluginMcpServers(mcpService, configurationResolverService, includeWorkspaceDotMcp); + const mcpServers = await collectNonPluginMcpServers(mcpService, configurationResolverService, sessionType, includeWorkspaceDotMcp); if (looseFiles.length > 0 || mcpServers.length > 0) { refs.push(bundler.bundle(looseFiles, mcpServers).then(r => r?.ref)); } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts index f27b1980005..bc17be7be4f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts @@ -12,6 +12,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { PluginFormat } from '../../../../../../platform/agentPlugins/common/pluginParsers.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { McpServerType } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js'; import { resolveCustomizationRefs, resolveLocalCustomAgents, shouldSyncWorkspaceDotMcp } from '../../../browser/agentSessions/agentHost/agentHostLocalCustomizations.js'; @@ -114,9 +115,9 @@ function makeFileService(stats: ReadonlyMap = new Map } as unknown as IFileService; } -function makeMcpServer(options: { id: string; collectionId: string; label?: string; enabled?: boolean; launch?: McpServerLaunch | undefined; configTarget?: ConfigurationTarget }): IMcpServer { - const { id, collectionId, label = id, enabled = true, launch, configTarget = ConfigurationTarget.USER } = options; - const collection = { id: collectionId, label: collectionId, order: 0, configTarget } as unknown as McpCollectionDefinition; +function makeMcpServer(options: { id: string; collectionId: string; label?: string; enabled?: boolean; launch?: McpServerLaunch | undefined; configTarget?: ConfigurationTarget; collectionSource?: ExtensionIdentifier }): IMcpServer { + const { id, collectionId, label = id, enabled = true, launch, configTarget = ConfigurationTarget.USER, collectionSource } = options; + const collection = { id: collectionId, label: collectionId, order: 0, configTarget, source: collectionSource } as unknown as McpCollectionDefinition; const definitions = observableValue('definitions', { server: launch ? { launch } : undefined, collection }); return { definition: { id, label }, @@ -143,6 +144,16 @@ const stdioLaunch: McpServerLaunch = { sandbox: undefined, }; +function makeCopilotChatGitHubMcpServer(): IMcpServer { + return makeMcpServer({ + id: 'github.copilot-chat/GitHub', + collectionId: 'github.copilot-chat/github', + label: 'GitHub', + launch: stdioLaunch, + collectionSource: new ExtensionIdentifier('GitHub.copilot-chat'), + }); +} + const stdioLaunchWithInput: McpServerLaunch = { type: McpServerTransportType.Stdio, command: 'my-server', @@ -480,6 +491,73 @@ suite('resolveCustomizationRefs - built-in skills', () => { assert.strictEqual(refs[0].name, 'Open Plugin'); }); + test('excludes the Copilot Chat GitHub MCP provider without excluding user or other extension servers', async () => { + const bundler = new FakeBundler(); + const mcpService = makeMcpService([ + makeCopilotChatGitHubMcpServer(), + makeMcpServer({ id: 'user.GitHub', collectionId: 'user', label: 'GitHub', launch: stdioLaunch }), + makeMcpServer({ + id: 'publisher.extension/server', + collectionId: 'publisher.extension/provider', + label: 'extension-server', + launch: stdioLaunch, + collectionSource: new ExtensionIdentifier('publisher.extension'), + }), + ]); + + await resolveCustomizationRefs( + makeFileService(), + makePromptsService(new Map()), + new FakeSyncProvider(), + makeAgentPluginService(), + mcpService, + makeConfigurationResolverService(), + bundler as unknown as SyncedCustomizationBundler, + 'agent-host-copilotcli', + ); + + assert.deepStrictEqual(bundler.receivedMcp, [[ + { name: 'GitHub', configuration: { type: McpServerType.LOCAL, command: 'my-server', args: ['--flag'], env: undefined, envFile: undefined, cwd: undefined } }, + { name: 'extension-server', configuration: { type: McpServerType.LOCAL, command: 'my-server', args: ['--flag'], env: undefined, envFile: undefined, cwd: undefined } }, + ]]); + }); + + test('excludes the Copilot Chat GitHub MCP provider from remote Copilot agent hosts', async () => { + const bundler = new FakeBundler(); + + await resolveCustomizationRefs( + makeFileService(), + makePromptsService(new Map()), + new FakeSyncProvider(), + makeAgentPluginService(), + makeMcpService([makeCopilotChatGitHubMcpServer()]), + makeConfigurationResolverService(), + bundler as unknown as SyncedCustomizationBundler, + 'remote-test-copilotcli', + ); + + assert.deepStrictEqual(bundler.receivedMcp, []); + }); + + test('retains the Copilot Chat GitHub MCP provider for agent hosts without a built-in server', async () => { + const bundler = new FakeBundler(); + + await resolveCustomizationRefs( + makeFileService(), + makePromptsService(new Map()), + new FakeSyncProvider(), + makeAgentPluginService(), + makeMcpService([makeCopilotChatGitHubMcpServer()]), + makeConfigurationResolverService(), + bundler as unknown as SyncedCustomizationBundler, + 'agent-host-claude', + ); + + assert.deepStrictEqual(bundler.receivedMcp, [[ + { name: 'GitHub', configuration: { type: McpServerType.LOCAL, command: 'my-server', args: ['--flag'], env: undefined, envFile: undefined, cwd: undefined } }, + ]]); + }); + test('excludes plugin-sourced MCP servers from the bundle', async () => { const bundler = new FakeBundler(); const mcpService = makeMcpService([