agentHost: sync .vscode/mcp.json servers, enable SDK config discovery (#324129)

Refine how VS Code-configured MCP servers reach the agent host:

- resolveCustomizationRefs now excludes workspace-discovered MCP servers
  by default, since the agent host discovers workspace `.mcp.json` itself
  (via enableConfigDiscovery). The exception is `.vscode/mcp.json`, which
  the SDK does not discover: those servers are synced explicitly, but only
  when their config resolves without user interaction. Resolution uses
  IConfigurationResolverService.resolveAsync + ConfigurationResolverExpression
  and skips servers that reference interactive variables (${input:…},
  ${command:…}) or fail to resolve.

- Enable `enableConfigDiscovery` when creating/resuming Copilot agent-host
  sessions so the host discovers workspace `.mcp.json` on its own.

- Move MCP_CONFIGURATION_COLLECTION_ID_PREFIX and add
  McpCollectionDefinition.isWorkspaceDiscovered / isVscodeMcpJson helpers to
  mcpTypes so the collection-classification logic lives in one place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Connor Peet
2026-07-02 17:41:08 -07:00
committed by GitHub
parent f15105f6ed
commit 2a41b4f7de
6 changed files with 332 additions and 13 deletions

View File

@@ -515,6 +515,7 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
clientName: 'vscode',
enableMcpApps: true,
enableFileHooks: true,
enableConfigDiscovery: true,
onPermissionRequest: request => runtime.handlePermissionRequest(request),
onUserInputRequest: (request, invocation) => runtime.handleUserInputRequest(request, invocation),
onElicitationRequest: context => runtime.handleElicitationRequest(context),

View File

@@ -19,6 +19,7 @@ import { IAgentPluginService } from '../../../common/plugins/agentPluginService.
import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js';
import { ILanguageModelToolsService, IToolData, IToolSet } from '../../../common/tools/languageModelToolsService.js';
import { IMcpService } from '../../../../mcp/common/mcpTypes.js';
import { IConfigurationResolverService } from '../../../../../services/configurationResolver/common/configurationResolver.js';
import { AgentCustomizationSyncProvider } from './agentCustomizationSyncProvider.js';
import { resolveCustomizationRefs } from './agentHostLocalCustomizations.js';
import { toolDataToDefinition } from './agentHostToolUtils.js';
@@ -82,6 +83,7 @@ export class AgentHostActiveClientService extends Disposable implements IAgentHo
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IFileService private readonly _fileService: IFileService,
@IMcpService private readonly _mcpService: IMcpService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
@IAgentHostToolSetEnablementService private readonly _toolSetEnablementService: IAgentHostToolSetEnablementService,
) {
super();
@@ -101,7 +103,7 @@ export class AgentHostActiveClientService extends Disposable implements IAgentHo
const updateCustomizations = async () => {
const seq = ++updateSeq;
try {
const refs = await resolveCustomizationRefs(this._fileService, this._promptsService, syncProvider, this._agentPluginService, this._mcpService, bundler, sessionType);
const refs = await resolveCustomizationRefs(this._fileService, this._promptsService, syncProvider, this._agentPluginService, this._mcpService, this._configurationResolverService, bundler, sessionType);
if (seq !== updateSeq) {
return;
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { Iterable } from '../../../../../../base/common/iterator.js';
import { isEqualOrParent } from '../../../../../../base/common/resources.js';
import { URI } from '../../../../../../base/common/uri.js';
import { CustomizationType, type URI as ProtocolURI } from '../../../../../../platform/agentHost/common/state/protocol/state.js';
@@ -16,7 +17,10 @@ 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, McpServerLaunch, McpServerTransportType } from '../../../../mcp/common/mcpTypes.js';
import { 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';
import type { ISyncableMcpServer, SyncedCustomizationBundler } from './syncedCustomizationBundler.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import { isDefined } from '../../../../../../base/common/types.js';
@@ -155,6 +159,53 @@ function launchToMcpServerConfiguration(launch: McpServerLaunch): IMcpServerConf
}
}
/**
* Attempts to resolve every configuration variable (`${workspaceFolder}`,
* `${env:…}`, …) in an MCP server config without any user interaction, using
* {@link IConfigurationResolverService.resolveAsync}. Returns the resolved
* config, or `undefined` when it cannot be fully resolved without prompting the
* user.
*
* The synced `.mcp.json` is launched by the agent host verbatim, so any
* variable the agent host can't itself expand must be resolved here up front.
* Variables requiring interaction (`${input:…}`, `${command:…}`) or context we
* don't have (e.g. `${workspaceFolder}` outside a folder) cause the server to
* be skipped.
*/
async function resolveConfigurationForSync(
configurationResolverService: IConfigurationResolverService,
folder: IWorkspaceFolderData | undefined,
configuration: IMcpServerConfiguration,
): Promise<IMcpServerConfiguration | undefined> {
const expr = ConfigurationResolverExpression.parse(configuration);
// Interactive variables (`${input:…}`, `${command:…}`) can only be resolved
// by prompting the user, so a server referencing them is skipped. This is
// checked up front because `resolveAsync` "resolves" them to their own
// literal text when no value mapping is supplied, which would otherwise
// leave them out of `unresolved()` below.
for (const replacement of expr.unresolved()) {
if (replacement.name === 'input' || replacement.name === 'command') {
return undefined;
}
}
try {
// Resolves everything that can be resolved without interaction; throws
// when a variable requires context we don't have (e.g. no folder).
await configurationResolverService.resolveAsync(folder, expr);
} catch {
return undefined;
}
// Any replacement left unresolved would require user interaction.
if (!Iterable.isEmpty(expr.unresolved())) {
return undefined;
}
return expr.toObject();
}
/**
* Enumerates MCP servers configured directly in VS Code — i.e. those that
* are not contributed by an agent plugin — so they can be bundled into the
@@ -162,8 +213,15 @@ function launchToMcpServerConfiguration(launch: McpServerLaunch): IMcpServerConf
* are already synced via their owning plugin's customization ref. Disabled
* servers and servers whose launch cannot be expressed declaratively are
* skipped.
*
* Workspace-discovered servers are also excluded by default: the agent host
* discovers workspace `.mcp.json` itself, so syncing them would duplicate. The
* 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.
*/
export function collectNonPluginMcpServers(mcpService: IMcpService): ISyncableMcpServer[] {
export async function collectNonPluginMcpServers(mcpService: IMcpService, configurationResolverService: IConfigurationResolverService): Promise<ISyncableMcpServer[]> {
const result: ISyncableMcpServer[] = [];
for (const server of mcpService.servers.get()) {
if (server.collection.id.startsWith(MCP_PLUGIN_COLLECTION_ID_PREFIX)) {
@@ -172,14 +230,27 @@ export function collectNonPluginMcpServers(mcpService: IMcpService): ISyncableMc
if (!isContributionEnabled(server.enablement.get())) {
continue;
}
const launch = server.readDefinitions().get().server?.launch;
const definitions = server.readDefinitions().get();
const definition = definitions.server;
const launch = definition?.launch;
if (!launch) {
continue;
}
const configuration = launchToMcpServerConfiguration(launch);
let configuration = launchToMcpServerConfiguration(launch);
if (!configuration) {
continue;
}
const collection = definitions.collection;
if (collection && McpCollectionDefinition.isWorkspaceDiscovered(collection)) {
if (!McpCollectionDefinition.isVscodeMcpJson(collection)) {
continue;
}
const resolved = await resolveConfigurationForSync(configurationResolverService, definition.variableReplacement?.folder, configuration);
if (!resolved) {
continue;
}
configuration = resolved;
}
result.push({ name: server.definition.label, configuration });
}
return result;
@@ -200,6 +271,7 @@ export async function resolveCustomizationRefs(
syncProvider: ICustomizationSyncProvider,
agentPluginService: IAgentPluginService,
mcpService: IMcpService,
configurationResolverService: IConfigurationResolverService,
bundler: SyncedCustomizationBundler,
sessionType: string,
): Promise<ClientPluginCustomization[]> {
@@ -272,7 +344,7 @@ export async function resolveCustomizationRefs(
}
const refs: Promise<ClientPluginCustomization | undefined>[] = [...pluginRefs.values()];
const mcpServers = collectNonPluginMcpServers(mcpService);
const mcpServers = await collectNonPluginMcpServers(mcpService, configurationResolverService);
if (looseFiles.length > 0 || mcpServers.length > 0) {
refs.push(bundler.bundle(looseFiles, mcpServers).then(r => r?.ref));
}

View File

@@ -9,6 +9,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js';
import { observableValue } from '../../../../../../base/common/observable.js';
import { URI } from '../../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import { McpServerType } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js';
import { resolveCustomizationRefs } from '../../../browser/agentSessions/agentHost/agentHostLocalCustomizations.js';
@@ -19,13 +20,39 @@ import { ContributionEnablementState } from '../../../common/enablement.js';
import { type IAgentPlugin, type IAgentPluginService } from '../../../common/plugins/agentPluginService.js';
import { PromptsType } from '../../../common/promptSyntax/promptTypes.js';
import { type IPromptPath, type IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
import { type IMcpServer, type IMcpService, McpServerLaunch, McpServerTransportType } from '../../../../mcp/common/mcpTypes.js';
import { type IMcpServer, type 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 { SessionType } from '../../../common/chatSessionsService.js';
function makePromptPath(uri: URI, type: PromptsType, storage: PromptsStorage): IPromptPath {
return { uri, type, storage } as IPromptPath;
}
/**
* A fake {@link IConfigurationResolverService} whose `resolveAsync` mirrors the
* real service: it resolves the given `${...}` variables from `resolutions` and
* leaves any others (e.g. `${input:…}`) untouched so they remain unresolved.
*/
function makeConfigurationResolverService(resolutions: Record<string, string> = {}): IConfigurationResolverService {
return {
async resolveAsync(_folder: unknown, config: unknown) {
const expr = ConfigurationResolverExpression.parse(config as object);
for (const replacement of expr.unresolved()) {
if (Object.prototype.hasOwnProperty.call(resolutions, replacement.id)) {
expr.resolve(replacement, resolutions[replacement.id]);
} else if (replacement.name === 'input' || replacement.name === 'command') {
// Mirror the real resolver: without a value mapping, interactive
// variables "resolve" to their own literal text, dropping out of
// `unresolved()`.
expr.resolve(replacement, replacement.id);
}
}
return expr.toObject();
},
} as unknown as IConfigurationResolverService;
}
function makePromptsService(files: ReadonlyMap<string, readonly IPromptPath[]>): IPromptsService {
return {
async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage): Promise<readonly IPromptPath[]> {
@@ -72,9 +99,10 @@ 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 }): IMcpServer {
const { id, collectionId, label = id, enabled = true, launch } = options;
const definitions = observableValue('definitions', { server: launch ? { launch } : undefined, collection: undefined });
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;
const definitions = observableValue('definitions', { server: launch ? { launch } : undefined, collection });
return {
definition: { id, label },
collection: { id: collectionId, label: collectionId, order: 0 },
@@ -100,6 +128,26 @@ const stdioLaunch: McpServerLaunch = {
sandbox: undefined,
};
const stdioLaunchWithInput: McpServerLaunch = {
type: McpServerTransportType.Stdio,
command: 'my-server',
args: ['--token', '${input:token}'],
env: {},
envFile: undefined,
cwd: undefined,
sandbox: undefined,
};
const stdioLaunchWithFolder: McpServerLaunch = {
type: McpServerTransportType.Stdio,
command: 'my-server',
args: ['--root', '${workspaceFolder}'],
env: {},
envFile: undefined,
cwd: undefined,
sandbox: undefined,
};
type LocalSyncableFile = { readonly uri: URI; readonly type: PromptsType };
class FakeBundler {
@@ -133,6 +181,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
makeMcpService(),
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -162,6 +211,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(new Set([disabled.toString()])),
makeAgentPluginService(),
makeMcpService(),
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -184,6 +234,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
makeMcpService(),
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -211,6 +262,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(new Set([builtin.toString()])),
makeAgentPluginService(),
makeMcpService(),
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -230,6 +282,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService([makePlugin(pluginUri, { label: 'MCP Only', mcpServers: 1 })]),
makeMcpService(),
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -248,6 +301,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService([makePlugin(pluginUri, { enabled: false, mcpServers: 1 })]),
makeMcpService(),
makeConfigurationResolverService(),
new FakeBundler() as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -265,6 +319,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService([makePlugin(pluginUri, { enabled: false })]),
makeMcpService(),
makeConfigurationResolverService(),
new FakeBundler() as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -279,6 +334,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(new Set([pluginUri.toString()])),
makeAgentPluginService([makePlugin(pluginUri, { mcpServers: 1 })]),
makeMcpService(),
makeConfigurationResolverService(),
new FakeBundler() as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -297,6 +353,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService([makePlugin(pluginUri, { label: 'Combined', mcpServers: 2 })]),
makeMcpService(),
makeConfigurationResolverService(),
new FakeBundler() as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -313,6 +370,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
makeMcpService(),
makeConfigurationResolverService(),
new FakeBundler() as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -333,6 +391,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -357,6 +416,7 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
@@ -378,10 +438,165 @@ suite('resolveCustomizationRefs - built-in skills', () => {
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 0);
});
test('excludes workspace-discovered `.mcp.json` servers (the agent host discovers those itself)', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'wsdot.srv', collectionId: 'workspace-dot-mcp.0', label: 'srv', launch: stdioLaunch, configTarget: ConfigurationTarget.WORKSPACE_FOLDER }),
]);
await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 0);
});
test('excludes `.code-workspace` configured servers', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'wscfg.srv', collectionId: 'mcp.config.workspace', label: 'srv', launch: stdioLaunch, configTarget: ConfigurationTarget.WORKSPACE }),
]);
await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 0);
});
test('syncs `.vscode/mcp.json` servers that resolve without user interaction', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'mcp.config.ws0.my-server', collectionId: 'mcp.config.ws0', label: 'my-server', launch: stdioLaunch, configTarget: ConfigurationTarget.WORKSPACE_FOLDER }),
]);
const refs = await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 1);
assert.deepStrictEqual(bundler.receivedMcp[0], [
{ name: 'my-server', configuration: { type: McpServerType.LOCAL, command: 'my-server', args: ['--flag'], env: undefined, envFile: undefined, cwd: undefined } },
]);
assert.strictEqual(refs.length, 1);
assert.strictEqual(refs[0].name, 'Open Plugin');
});
test('excludes `.vscode/mcp.json` servers with variables that require interaction (e.g. ${input:…})', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'mcp.config.ws0.needs-input', collectionId: 'mcp.config.ws0', label: 'needs-input', launch: stdioLaunchWithInput, configTarget: ConfigurationTarget.WORKSPACE_FOLDER }),
]);
await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 0);
});
test('syncs `.vscode/mcp.json` servers after resolving non-interactive variables (e.g. ${workspaceFolder})', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'mcp.config.ws0.folder', collectionId: 'mcp.config.ws0', label: 'folder-server', launch: stdioLaunchWithFolder, configTarget: ConfigurationTarget.WORKSPACE_FOLDER }),
]);
const refs = await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService({ '${workspaceFolder}': '/ws' }),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 1);
assert.deepStrictEqual(bundler.receivedMcp[0], [
{ name: 'folder-server', configuration: { type: McpServerType.LOCAL, command: 'my-server', args: ['--root', '/ws'], env: undefined, envFile: undefined, cwd: undefined } },
]);
assert.strictEqual(refs.length, 1);
});
test('excludes `.vscode/mcp.json` servers when variable resolution throws', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'mcp.config.ws0.folder', collectionId: 'mcp.config.ws0', label: 'folder-server', launch: stdioLaunchWithFolder, configTarget: ConfigurationTarget.WORKSPACE_FOLDER }),
]);
const throwingResolver = {
async resolveAsync() { throw new Error('no workspace folder'); },
} as unknown as IConfigurationResolverService;
await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
throwingResolver,
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 0);
});
test('still syncs extension-contributed servers (workspace scope, user config target)', async () => {
const bundler = new FakeBundler();
const mcpService = makeMcpService([
makeMcpServer({ id: 'ext.foo.srv', collectionId: 'ext.foo', label: 'srv', launch: stdioLaunch, configTarget: ConfigurationTarget.USER }),
]);
const refs = await resolveCustomizationRefs(
makeFileService(),
makePromptsService(new Map()),
new FakeSyncProvider(),
makeAgentPluginService(),
mcpService,
makeConfigurationResolverService(),
bundler as unknown as SyncedCustomizationBundler,
SessionType.CopilotCLI,
);
assert.strictEqual(bundler.received.length, 1);
assert.deepStrictEqual(bundler.receivedMcp[0].map(s => s.name), ['srv']);
assert.strictEqual(refs.length, 1);
});
});

View File

@@ -18,7 +18,7 @@ import { IWorkbenchLocalMcpServer } from '../../../../services/mcp/common/mcpWor
import { getMcpServerMapping } from '../mcpConfigFileUtils.js';
import { mcpConfigurationSection } from '../mcpConfiguration.js';
import { IMcpRegistry } from '../mcpRegistryTypes.js';
import { IMcpConfigPath, IMcpWorkbenchService, McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';
import { IMcpConfigPath, IMcpWorkbenchService, MCP_CONFIGURATION_COLLECTION_ID_PREFIX, McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';
import { IMcpDiscovery } from './mcpDiscovery.js';
interface CollectionState extends IDisposable {
@@ -77,7 +77,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc
const config = server.config;
const mcpConfigPath = await mcpConfigPathPromise;
const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;
const collectionId = `${MCP_CONFIGURATION_COLLECTION_ID_PREFIX}${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;
let definitions = collections.get(collectionId);
if (!definitions) {

View File

@@ -27,7 +27,7 @@ import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServe
import { IMcpDevModeConfig, IMcpSandboxConfiguration, IMcpServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
import { IWorkspaceFolder, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions, WORKSPACE_FOLDER_CONFIG_ID_PREFIX } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { ContributionEnablementState, IEnablementModel } from '../../chat/common/enablement.js';
import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js';
import { IMcpServerSamplingConfiguration } from './mcpConfiguration.js';
@@ -37,6 +37,14 @@ import { UriTemplate } from '../../../../base/common/uriTemplate.js';
export const extensionMcpCollectionPrefix = 'ext.';
/**
* Prefix of the collection id used for MCP servers configured via the various
* `mcp.json`-style config files (user, remote user, workspace, and
* `.vscode/mcp.json` workspace-folder configs). The suffix is the
* {@link IMcpConfigPath.id} of the originating config path.
*/
export const MCP_CONFIGURATION_COLLECTION_ID_PREFIX = 'mcp.config.';
export function extensionPrefixedIdentifier(identifier: ExtensionIdentifier, id: string): string {
return ExtensionIdentifier.toKey(identifier) + '/' + id;
}
@@ -119,6 +127,27 @@ export namespace McpCollectionDefinition {
&& a.trustBehavior === b.trustBehavior
&& objectsEqual(a.sandbox, b.sandbox);
}
/**
* Returns `true` when the collection was discovered from the workspace (its
* config target is the workspace or a workspace folder). This is
* intentionally based on the config target and not the storage scope:
* extension-contributed collections use a workspace storage scope but are
* configured at the user level, so they are not workspace-discovered.
*/
export function isWorkspaceDiscovered(collection: McpCollectionDefinition): boolean {
return collection.configTarget === ConfigurationTarget.WORKSPACE
|| collection.configTarget === ConfigurationTarget.WORKSPACE_FOLDER;
}
/**
* Returns `true` when the collection originates from a `.vscode/mcp.json`
* workspace-folder config, identified by its collection id prefix (the
* shared `mcp.config.` prefix plus the workspace-folder config id).
*/
export function isVscodeMcpJson(collection: McpCollectionDefinition): boolean {
return collection.id.startsWith(`${MCP_CONFIGURATION_COLLECTION_ID_PREFIX}${WORKSPACE_FOLDER_CONFIG_ID_PREFIX}`);
}
}
export interface McpServerDefinition {