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>
This commit is contained in:
Connor Peet
2026-08-07 16:53:13 +00:00
committed by GitHub
parent 1aa1a7b51a
commit 8eaafbf422
2 changed files with 103 additions and 8 deletions
@@ -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<ISyncableMcpServer[]> {
export async function collectNonPluginMcpServers(mcpService: IMcpService, configurationResolverService: IConfigurationResolverService, sessionType: string, includeWorkspaceDotMcp: boolean): Promise<ISyncableMcpServer[]> {
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<ClientPluginCustomization | undefined>[] = [...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));
}
@@ -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<string, { mtime: number }> = 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([