Implement first class resource for user mcp servers (#252277)

* Revert "Revert mcp.json changes (#252245)"

This reverts commit 94a2502601.

* fix change event installing mcp servers

* properly implement workspace mcp servers
This commit is contained in:
Sandeep Somavarapu 2025-06-24 14:10:40 +02:00 committed by GitHub
parent badd2956bb
commit c8a406e31f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2972 additions and 1046 deletions

View File

@ -134,7 +134,6 @@ export interface INativeEnvironmentService extends IEnvironmentService {
appSettingsHome: URI;
tmpDir: URI;
userDataPath: string;
machineSettingsResource: URI;
// --- extensions
extensionsPath: string;

View File

@ -82,9 +82,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
@memoize
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
@memoize
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get workspaceStorageHome(): URI { return joinPath(this.appSettingsHome, 'workspaceStorage'); }

View File

@ -4,16 +4,31 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../base/common/cancellation.js';
import { IStringDictionary } from '../../../base/common/collections.js';
import { Event } from '../../../base/common/event.js';
import { URI } from '../../../base/common/uri.js';
import { SortBy, SortOrder } from '../../extensionManagement/common/extensionManagement.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IMcpServerConfiguration } from './mcpPlatformTypes.js';
import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js';
export interface IScannedMcpServers {
servers?: IStringDictionary<IScannedMcpServer>;
inputs?: IMcpServerVariable[];
}
export interface IScannedMcpServer {
readonly id: string;
readonly name: string;
readonly version: string;
readonly gallery?: boolean;
readonly config: IMcpServerConfiguration;
}
export interface ILocalMcpServer {
readonly name: string;
readonly config: IMcpServerConfiguration;
readonly version: string;
readonly mcpResource: URI;
readonly location?: URI;
readonly id?: string;
readonly displayName?: string;
@ -118,36 +133,6 @@ export interface IQueryOptions {
sortOrder?: SortOrder;
}
export interface InstallMcpServerEvent {
readonly name: string;
readonly source?: IGalleryMcpServer;
readonly applicationScoped?: boolean;
readonly workspaceScoped?: boolean;
}
export interface InstallMcpServerResult {
readonly name: string;
readonly source?: IGalleryMcpServer;
readonly local?: ILocalMcpServer;
readonly error?: Error;
readonly applicationScoped?: boolean;
readonly workspaceScoped?: boolean;
}
export interface UninstallMcpServerEvent {
readonly name: string;
readonly applicationScoped?: boolean;
readonly workspaceScoped?: boolean;
}
export interface DidUninstallMcpServerEvent {
readonly name: string;
readonly error?: string;
readonly applicationScoped?: boolean;
readonly workspaceScoped?: boolean;
}
export const IMcpGalleryService = createDecorator<IMcpGalleryService>('IMcpGalleryService');
export interface IMcpGalleryService {
readonly _serviceBrand: undefined;
@ -157,6 +142,46 @@ export interface IMcpGalleryService {
getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise<string>;
}
export interface InstallMcpServerEvent {
readonly name: string;
readonly mcpResource: URI;
readonly source?: IGalleryMcpServer;
}
export interface InstallMcpServerResult {
readonly name: string;
readonly mcpResource: URI;
readonly source?: IGalleryMcpServer;
readonly local?: ILocalMcpServer;
readonly error?: Error;
}
export interface UninstallMcpServerEvent {
readonly name: string;
readonly mcpResource: URI;
}
export interface DidUninstallMcpServerEvent {
readonly name: string;
readonly mcpResource: URI;
readonly error?: string;
}
export type InstallOptions = {
packageType?: PackageType;
mcpResource?: URI;
};
export type UninstallOptions = {
mcpResource?: URI;
};
export interface IMcpServer {
name: string;
config: IMcpServerConfiguration;
inputs?: IMcpServerVariable[];
}
export const IMcpManagementService = createDecorator<IMcpManagementService>('IMcpManagementService');
export interface IMcpManagementService {
readonly _serviceBrand: undefined;
@ -164,9 +189,10 @@ export interface IMcpManagementService {
readonly onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
readonly onUninstallMcpServer: Event<UninstallMcpServerEvent>;
readonly onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
getInstalled(): Promise<ILocalMcpServer[]>;
installFromGallery(server: IGalleryMcpServer, packageType: PackageType): Promise<void>;
uninstall(server: ILocalMcpServer): Promise<void>;
getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;
install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;
}
export const mcpGalleryServiceUrlConfig = 'chat.mcp.gallery.serviceUrl';

View File

@ -3,39 +3,32 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { ILogger } from '../../log/common/log.js';
import { IMcpConfiguration, IMcpConfigurationHTTP, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js';
import { IMcpServerConfiguration } from './mcpPlatformTypes.js';
import { IMcpManagementService } from './mcpManagement.js';
type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationHTTP };
type ValidatedConfig = { name: string; config: IMcpServerConfiguration };
export class McpManagementCli {
constructor(
private readonly _logger: ILogger,
@IConfigurationService private readonly _userConfigurationService: IConfigurationService,
@IMcpManagementService private readonly _mcpManagementService: IMcpManagementService,
) { }
async addMcpDefinitions(
definitions: string[],
) {
const configs = definitions.map((config) => this.validateConfiguration(config));
await this.updateMcpInConfig(this._userConfigurationService, configs);
await this.updateMcpInResource(configs);
this._logger.info(`Added MCP servers: ${configs.map(c => c.name).join(', ')}`);
}
private async updateMcpInConfig(service: IConfigurationService, configs: ValidatedConfig[]) {
const mcp = service.getValue<IMcpConfiguration>('mcp') || { servers: {} };
mcp.servers ??= {};
for (const config of configs) {
mcp.servers[config.name] = config.config;
}
await service.updateValue('mcp', mcp);
private async updateMcpInResource(configs: ValidatedConfig[]) {
await Promise.all(configs.map(({ name, config }) => this._mcpManagementService.install({ name, config })));
}
private validateConfiguration(config: string): ValidatedConfig {
let parsed: McpConfigurationServer & { name: string };
let parsed: IMcpServerConfiguration & { name: string };
try {
parsed = JSON.parse(config);
} catch (e) {
@ -51,7 +44,7 @@ export class McpManagementCli {
}
const { name, ...rest } = parsed;
return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationHTTP };
return { name, config: rest as IMcpServerConfiguration };
}
}

View File

@ -0,0 +1,145 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { cloneAndChange } from '../../../base/common/objects.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from '../../../base/common/uriIpc.js';
import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpManagementService, IMcpServer, InstallMcpServerEvent, InstallMcpServerResult, InstallOptions, UninstallMcpServerEvent, UninstallOptions } from './mcpManagement.js';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI;
function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined;
function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined {
return uri ? URI.revive(transformer ? transformer.transformIncoming(uri) : uri) : undefined;
}
function transformIncomingServer(mcpServer: ILocalMcpServer, transformer: IURITransformer | null): ILocalMcpServer {
transformer = transformer ? transformer : DefaultURITransformer;
const manifest = mcpServer.manifest;
const transformed = transformAndReviveIncomingURIs({ ...mcpServer, ...{ manifest: undefined } }, transformer);
return { ...transformed, ...{ manifest } };
}
function transformIncomingOptions<O extends { mcpResource?: UriComponents }>(options: O | undefined, transformer: IURITransformer | null): O | undefined {
return options?.mcpResource ? transformAndReviveIncomingURIs(options, transformer ?? DefaultURITransformer) : options;
}
function transformOutgoingExtension(extension: ILocalMcpServer, transformer: IURITransformer | null): ILocalMcpServer {
return transformer ? cloneAndChange(extension, value => value instanceof URI ? transformer.transformOutgoingURI(value) : undefined) : extension;
}
function transformOutgoingURI(uri: URI, transformer: IURITransformer | null): URI {
return transformer ? transformer.transformOutgoingURI(uri) : uri;
}
export class McpManagementChannel implements IServerChannel {
readonly onInstallMcpServer: Event<InstallMcpServerEvent>;
readonly onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
readonly onUninstallMcpServer: Event<UninstallMcpServerEvent>;
readonly onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
constructor(private service: IMcpManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) {
this.onInstallMcpServer = Event.buffer(service.onInstallMcpServer, true);
this.onDidInstallMcpServers = Event.buffer(service.onDidInstallMcpServers, true);
this.onUninstallMcpServer = Event.buffer(service.onUninstallMcpServer, true);
this.onDidUninstallMcpServer = Event.buffer(service.onDidUninstallMcpServer, true);
}
listen(context: any, event: string): Event<any> {
const uriTransformer = this.getUriTransformer(context);
switch (event) {
case 'onInstallMcpServer': {
return Event.map<InstallMcpServerEvent, InstallMcpServerEvent>(this.onInstallMcpServer, event => {
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
});
}
case 'onDidInstallMcpServers': {
return Event.map<readonly InstallMcpServerResult[], readonly InstallMcpServerResult[]>(this.onDidInstallMcpServers, results =>
results.map(i => ({
...i,
local: i.local ? transformOutgoingExtension(i.local, uriTransformer) : i.local,
mcpResource: transformOutgoingURI(i.mcpResource, uriTransformer)
})));
}
case 'onUninstallMcpServer': {
return Event.map<UninstallMcpServerEvent, UninstallMcpServerEvent>(this.onUninstallMcpServer, event => {
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
});
}
case 'onDidUninstallMcpServer': {
return Event.map<DidUninstallMcpServerEvent, DidUninstallMcpServerEvent>(this.onDidUninstallMcpServer, event => {
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
});
}
}
throw new Error('Invalid listen');
}
async call(context: any, command: string, args?: any): Promise<any> {
const uriTransformer: IURITransformer | null = this.getUriTransformer(context);
switch (command) {
case 'getInstalled': {
const mcpServers = await this.service.getInstalled(transformIncomingURI(args[0], uriTransformer));
return mcpServers.map(e => transformOutgoingExtension(e, uriTransformer));
}
case 'install': {
return this.service.install(args[0], transformIncomingOptions(args[1], uriTransformer));
}
case 'installFromGallery': {
return this.service.installFromGallery(args[0], transformIncomingOptions(args[1], uriTransformer));
}
case 'uninstall': {
return this.service.uninstall(transformIncomingServer(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer));
}
}
throw new Error('Invalid call');
}
}
export class McpManagementChannelClient extends Disposable implements IMcpManagementService {
declare readonly _serviceBrand: undefined;
private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
get onInstallMcpServer() { return this._onInstallMcpServer.event; }
private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; }
private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
get onUninstallMcpServer() { return this._onUninstallMcpServer.event; }
private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }
constructor(private readonly channel: IChannel) {
super();
this._register(this.channel.listen<InstallMcpServerEvent>('onInstallMcpServer')(e => this._onInstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
this._register(this.channel.listen<readonly InstallMcpServerResult[]>('onDidInstallMcpServers')(results => this._onDidInstallMcpServers.fire(results.map(e => ({ ...e, local: e.local ? transformIncomingServer(e.local, null) : e.local, mcpResource: transformIncomingURI(e.mcpResource, null) })))));
this._register(this.channel.listen<UninstallMcpServerEvent>('onUninstallMcpServer')(e => this._onUninstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
this._register(this.channel.listen<DidUninstallMcpServerEvent>('onDidUninstallMcpServer')(e => this._onDidUninstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
}
install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
return Promise.resolve(this.channel.call<ILocalMcpServer>('install', [server, options])).then(local => transformIncomingServer(local, null));
}
installFromGallery(extension: IGalleryMcpServer, installOptions?: InstallOptions): Promise<ILocalMcpServer> {
return Promise.resolve(this.channel.call<ILocalMcpServer>('installFromGallery', [extension, installOptions])).then(local => transformIncomingServer(local, null));
}
uninstall(extension: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
return Promise.resolve(this.channel.call<void>('uninstall', [extension, options]));
}
getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
return Promise.resolve(this.channel.call<ILocalMcpServer[]>('getInstalled', [mcpResource]))
.then(servers => servers.map(server => transformIncomingServer(server, null)));
}
}

View File

@ -7,38 +7,39 @@ import { VSBuffer } from '../../../base/common/buffer.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { deepClone } from '../../../base/common/objects.js';
import { uppercaseFirstLetter } from '../../../base/common/strings.js';
import { URI } from '../../../base/common/uri.js';
import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js';
import { ConfigurationTarget } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IMcpServerManifest, InstallMcpServerEvent, InstallMcpServerResult, PackageType, UninstallMcpServerEvent } from './mcpManagement.js';
import { McpConfigurationServer, IMcpServerVariable, McpServerVariableType, IMcpServersConfiguration, IMcpServerConfiguration } from './mcpPlatformTypes.js';
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IMcpServerManifest, InstallMcpServerEvent, InstallMcpServerResult, PackageType, UninstallMcpServerEvent, IScannedMcpServer, InstallOptions, UninstallOptions, IMcpServer } from './mcpManagement.js';
import { IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration } from './mcpPlatformTypes.js';
import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js';
interface LocalMcpServer {
readonly name: string;
readonly version: string;
readonly id?: string;
readonly displayName?: string;
readonly url?: string;
readonly description?: string;
readonly repositoryUrl?: string;
readonly publisher?: string;
readonly publisherDisplayName?: string;
readonly iconUrl?: string;
readonly manifest?: IMcpServerManifest;
export interface ILocalMcpServerInfo {
name: string;
version: string;
id?: string;
displayName?: string;
url?: string;
description?: string;
repositoryUrl?: string;
publisher?: string;
publisherDisplayName?: string;
iconUrl?: string;
manifest?: IMcpServerManifest;
readmeUrl?: URI;
location?: URI;
}
export class McpManagementService extends Disposable implements IMcpManagementService {
export abstract class AbstractMcpManagementService extends Disposable implements IMcpManagementService {
_serviceBrand: undefined;
private readonly mcpLocation: URI;
private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
protected readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
readonly onInstallMcpServer = this._onInstallMcpServer.event;
protected readonly _onDidInstallMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());
@ -51,55 +52,47 @@ export class McpManagementService extends Disposable implements IMcpManagementSe
get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
@IFileService private readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@ILogService private readonly logService: ILogService,
protected readonly target: McpResourceTarget,
@IMcpGalleryService protected readonly mcpGalleryService: IMcpGalleryService,
@IFileService protected readonly fileService: IFileService,
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
@ILogService protected readonly logService: ILogService,
@IMcpResourceScannerService protected readonly mcpResourceScannerService: IMcpResourceScannerService,
) {
super();
this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp');
}
async getInstalled(): Promise<ILocalMcpServer[]> {
const { userLocal } = this.configurationService.inspect<IMcpServersConfiguration>('mcp');
async getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
const mcpResourceUri = mcpResource || this.getDefaultMcpResource();
this.logService.info('MCP Management Service: getInstalled', mcpResourceUri.toString());
if (!userLocal?.value?.servers) {
try {
const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(mcpResourceUri, this.target);
if (!scannedMcpServers.servers) {
return [];
}
return Promise.all(Object.entries(scannedMcpServers.servers).map(([, scannedServer]) => this.scanServer(scannedServer, mcpResourceUri)));
} catch (error) {
this.logService.debug('Could not read user MCP servers:', error);
return [];
}
return Promise.all(Object.entries(userLocal.value.servers).map(([name, config]) => this.scanServer(name, config)));
}
private async scanServer(name: string, config: IMcpServerConfiguration): Promise<ILocalMcpServer> {
let scanned: LocalMcpServer | undefined;
let readmeUrl: URI | undefined;
if (config.location) {
const manifestLocation = this.uriIdentityService.extUri.joinPath(URI.revive(config.location), 'manifest.json');
try {
const content = await this.fileService.readFile(manifestLocation);
scanned = JSON.parse(content.value.toString());
} catch (e) {
this.logService.error('MCP Management Service: failed to read manifest', config.location.toString(), e);
}
readmeUrl = this.uriIdentityService.extUri.joinPath(URI.revive(config.location), 'README.md');
if (!await this.fileService.exists(readmeUrl)) {
readmeUrl = undefined;
}
}
if (!scanned) {
protected async scanServer(scannedMcpServer: IScannedMcpServer, mcpResource: URI): Promise<ILocalMcpServer> {
let mcpServerInfo = await this.getLocalMcpServerInfo(scannedMcpServer);
if (!mcpServerInfo) {
let publisher = '';
const nameParts = name.split('/');
const nameParts = scannedMcpServer.name.split('/');
if (nameParts.length > 0) {
const domainParts = nameParts[0].split('.');
if (domainParts.length > 0) {
publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner
}
}
scanned = {
name,
mcpServerInfo = {
name: scannedMcpServer.name,
version: '1.0.0',
displayName: nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '),
publisher
@ -107,115 +100,77 @@ export class McpManagementService extends Disposable implements IMcpManagementSe
}
return {
name,
config,
version: scanned.version,
location: URI.revive(config.location),
id: scanned.id,
displayName: scanned.displayName,
description: scanned.description,
publisher: scanned.publisher,
publisherDisplayName: scanned.publisherDisplayName,
repositoryUrl: scanned.repositoryUrl,
readmeUrl,
iconUrl: scanned.iconUrl,
manifest: scanned.manifest
name: scannedMcpServer.name,
config: scannedMcpServer.config,
version: mcpServerInfo.version,
mcpResource,
location: mcpServerInfo.location,
id: mcpServerInfo.id,
displayName: mcpServerInfo.displayName,
description: mcpServerInfo.description,
publisher: mcpServerInfo.publisher,
publisherDisplayName: mcpServerInfo.publisherDisplayName,
repositoryUrl: mcpServerInfo.repositoryUrl,
readmeUrl: mcpServerInfo.readmeUrl,
iconUrl: mcpServerInfo.iconUrl,
manifest: mcpServerInfo.manifest
};
}
async installFromGallery(server: IGalleryMcpServer, packageType?: PackageType): Promise<void> {
this.logService.trace('MCP Management Service: installGallery', server.url);
this._onInstallMcpServer.fire({ name: server.name });
async install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
this.logService.trace('MCP Management Service: install', server.name);
const mcpResource = options?.mcpResource ?? this.getDefaultMcpResource();
this._onInstallMcpServer.fire({ name: server.name, mcpResource });
try {
const manifest = await this.mcpGalleryService.getManifest(server, CancellationToken.None);
const location = this.uriIdentityService.extUri.joinPath(this.mcpLocation, `${server.name.replace('/', '.')}-${server.version}`);
const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify({
id: server.id,
const scannedServer: IScannedMcpServer = {
id: server.name,
name: server.name,
displayName: server.displayName,
description: server.description,
version: server.version,
publisher: server.publisher,
publisherDisplayName: server.publisherDisplayName,
repository: server.repositoryUrl,
licenseUrl: server.licenseUrl,
...manifest,
})));
if (server.readmeUrl) {
const readme = await this.mcpGalleryService.getReadme(server, CancellationToken.None);
await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
}
const { userLocal } = this.configurationService.inspect<IMcpServersConfiguration>('mcp');
const value: IMcpServersConfiguration = deepClone(userLocal?.value ?? { servers: {} });
if (!value.servers) {
value.servers = {};
}
const serverConfig = this.getServerConfig(manifest, packageType);
value.servers[server.name] = {
...serverConfig,
location: location.toJSON(),
version: '0.0.1',
config: server.config
};
if (serverConfig.inputs) {
value.inputs = value.inputs ?? [];
for (const input of serverConfig.inputs) {
if (!value.inputs.some(i => (<IMcpServerVariable>i).id === input.id)) {
value.inputs.push({ ...input, serverName: server.name });
}
}
}
await this.configurationService.updateValue('mcp', value, ConfigurationTarget.USER_LOCAL);
const local = await this.scanServer(server.name, value.servers[server.name]);
this._onDidInstallMcpServers.fire([{ name: server.name, source: server, local }]);
await this.mcpResourceScannerService.addMcpServers([{ server: scannedServer, inputs: server.inputs }], mcpResource, this.target);
const local = await this.scanServer(scannedServer, mcpResource);
this._onDidInstallMcpServers.fire([{ name: server.name, local, mcpResource }]);
return local;
} catch (e) {
this._onDidInstallMcpServers.fire([{ name: server.name, source: server, error: e }]);
this._onDidInstallMcpServers.fire([{ name: server.name, error: e, mcpResource }]);
throw e;
}
}
async uninstall(server: ILocalMcpServer): Promise<void> {
async uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
this.logService.trace('MCP Management Service: uninstall', server.name);
this._onUninstallMcpServer.fire({ name: server.name });
const mcpResource = options?.mcpResource ?? this.getDefaultMcpResource();
this._onUninstallMcpServer.fire({ name: server.name, mcpResource });
try {
const { userLocal } = this.configurationService.inspect<IMcpServersConfiguration>('mcp');
const value: IMcpServersConfiguration = deepClone(userLocal?.value ?? { servers: {} });
if (!value.servers) {
value.servers = {};
const currentServers = await this.mcpResourceScannerService.scanMcpServers(mcpResource, this.target);
if (!currentServers.servers) {
return;
}
delete value.servers[server.name];
if (value.inputs) {
const index = value.inputs.findIndex(i => (<IMcpServerVariable>i).serverName === server.name);
if (index !== undefined && index >= 0) {
value.inputs?.splice(index, 1);
}
}
await this.configurationService.updateValue('mcp', value, ConfigurationTarget.USER_LOCAL);
await this.mcpResourceScannerService.removeMcpServers([server.name], mcpResource, this.target);
if (server.location) {
await this.fileService.del(URI.revive(server.location), { recursive: true });
}
this._onDidUninstallMcpServer.fire({ name: server.name });
this._onDidUninstallMcpServer.fire({ name: server.name, mcpResource });
} catch (e) {
this._onDidUninstallMcpServer.fire({ name: server.name, error: e });
this._onDidUninstallMcpServer.fire({ name: server.name, error: e, mcpResource });
throw e;
}
}
private getServerConfig(manifest: IMcpServerManifest, packageType?: PackageType): McpConfigurationServer & { inputs?: IMcpServerVariable[] } {
protected toScannedMcpServerAndInputs(manifest: IMcpServerManifest, packageType?: PackageType): { config: IMcpServerConfiguration; inputs?: IMcpServerVariable[] } {
if (packageType === undefined) {
packageType = manifest.packages?.[0]?.registry_name ?? PackageType.REMOTE;
}
let config: IMcpServerConfiguration;
const inputs: IMcpServerVariable[] = [];
if (packageType === PackageType.REMOTE) {
const inputs: IMcpServerVariable[] = [];
const headers: Record<string, string> = {};
for (const input of manifest.remotes[0].headers ?? []) {
headers[input.name] = input.value;
@ -223,84 +178,86 @@ export class McpManagementService extends Disposable implements IMcpManagementSe
inputs.push(...this.getVariables(input.variables));
}
}
return {
config = {
type: 'http',
url: manifest.remotes[0].url,
headers: Object.keys(headers).length ? headers : undefined,
inputs: inputs.length ? inputs : undefined,
};
} else {
const serverPackage = manifest.packages.find(p => p.registry_name === packageType) ?? manifest.packages[0];
const args: string[] = [];
const env: Record<string, string> = {};
if (serverPackage.registry_name === PackageType.DOCKER) {
args.push('run');
args.push('-i');
args.push('--rm');
}
for (const arg of serverPackage.runtime_arguments ?? []) {
if (arg.type === 'positional') {
args.push(arg.value ?? arg.value_hint);
} else if (arg.type === 'named') {
args.push(arg.name);
if (arg.value) {
args.push(arg.value);
}
}
if (arg.variables) {
inputs.push(...this.getVariables(arg.variables));
}
}
for (const input of serverPackage.environment_variables ?? []) {
const variables = input.variables ? this.getVariables(input.variables) : [];
let value = input.value;
for (const variable of variables) {
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
}
env[input.name] = value;
if (variables.length) {
inputs.push(...variables);
}
if (serverPackage.registry_name === PackageType.DOCKER) {
args.push('-e');
args.push(input.name);
}
}
if (serverPackage.registry_name === PackageType.NODE) {
args.push(`${serverPackage.name}@${serverPackage.version}`);
}
else if (serverPackage.registry_name === PackageType.PYTHON) {
args.push(`${serverPackage.name}==${serverPackage.version}`);
}
else if (serverPackage.registry_name === PackageType.DOCKER) {
args.push(`${serverPackage.name}:${serverPackage.version}`);
}
for (const arg of serverPackage.package_arguments ?? []) {
if (arg.type === 'positional') {
args.push(arg.value ?? arg.value_hint);
} else if (arg.type === 'named') {
args.push(arg.name);
if (arg.value) {
args.push(arg.value);
}
}
if (arg.variables) {
inputs.push(...this.getVariables(arg.variables));
}
}
config = {
type: 'stdio',
command: this.getCommandName(serverPackage.registry_name),
args: args.length ? args : undefined,
env: Object.keys(env).length ? env : undefined,
};
}
const serverPackage = manifest.packages.find(p => p.registry_name === packageType) ?? manifest.packages[0];
const inputs: IMcpServerVariable[] = [];
const args: string[] = [];
const env: Record<string, string> = {};
if (serverPackage.registry_name === PackageType.DOCKER) {
args.push('run');
args.push('-i');
args.push('--rm');
}
for (const arg of serverPackage.runtime_arguments ?? []) {
if (arg.type === 'positional') {
args.push(arg.value ?? arg.value_hint);
} else if (arg.type === 'named') {
args.push(arg.name);
if (arg.value) {
args.push(arg.value);
}
}
if (arg.variables) {
inputs.push(...this.getVariables(arg.variables));
}
}
for (const input of serverPackage.environment_variables ?? []) {
const variables = input.variables ? this.getVariables(input.variables) : [];
let value = input.value;
for (const variable of variables) {
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
}
env[input.name] = value;
if (variables.length) {
inputs.push(...variables);
}
if (serverPackage.registry_name === PackageType.DOCKER) {
args.push('-e');
args.push(input.name);
}
}
if (serverPackage.registry_name === PackageType.NODE) {
args.push(`${serverPackage.name}@${serverPackage.version}`);
}
else if (serverPackage.registry_name === PackageType.PYTHON) {
args.push(`${serverPackage.name}==${serverPackage.version}`);
}
else if (serverPackage.registry_name === PackageType.DOCKER) {
args.push(`${serverPackage.name}:${serverPackage.version}`);
}
for (const arg of serverPackage.package_arguments ?? []) {
if (arg.type === 'positional') {
args.push(arg.value ?? arg.value_hint);
} else if (arg.type === 'named') {
args.push(arg.name);
if (arg.value) {
args.push(arg.value);
}
}
if (arg.variables) {
inputs.push(...this.getVariables(arg.variables));
}
}
return {
type: 'stdio',
command: this.getCommandName(serverPackage.registry_name),
args: args.length ? args : undefined,
env: Object.keys(env).length ? env : undefined,
config,
inputs: inputs.length ? inputs : undefined,
};
}
@ -329,4 +286,106 @@ export class McpManagementService extends Disposable implements IMcpManagementSe
return variables;
}
abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
protected abstract getDefaultMcpResource(): URI;
protected abstract getLocalMcpServerInfo(scannedMcpServer: IScannedMcpServer): Promise<ILocalMcpServerInfo | undefined>;
}
export class McpManagementService extends AbstractMcpManagementService implements IMcpManagementService {
private readonly mcpLocation: URI;
constructor(
@IMcpGalleryService mcpGalleryService: IMcpGalleryService,
@IFileService fileService: IFileService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@ILogService logService: ILogService,
@IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
) {
super(ConfigurationTarget.USER, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService);
this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp');
}
async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
this.logService.trace('MCP Management Service: installGallery', server.url);
const mcpResource = options?.mcpResource ?? this.getDefaultMcpResource();
this._onInstallMcpServer.fire({ name: server.name, mcpResource });
try {
const manifest = await this.mcpGalleryService.getManifest(server, CancellationToken.None);
const location = this.getLocation(server.name, server.version);
const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify({
id: server.id,
name: server.name,
displayName: server.displayName,
description: server.description,
version: server.version,
publisher: server.publisher,
publisherDisplayName: server.publisherDisplayName,
repository: server.repositoryUrl,
licenseUrl: server.licenseUrl,
...manifest,
})));
if (server.readmeUrl) {
const readme = await this.mcpGalleryService.getReadme(server, CancellationToken.None);
await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
}
const { config, inputs } = this.toScannedMcpServerAndInputs(manifest, options?.packageType);
const scannedServer: IScannedMcpServer = {
id: server.id,
name: server.name,
version: server.version,
gallery: true,
config
};
await this.mcpResourceScannerService.addMcpServers([{ server: scannedServer, inputs }], mcpResource, this.target);
const local = await this.scanServer(scannedServer, mcpResource);
this._onDidInstallMcpServers.fire([{ name: server.name, source: server, local, mcpResource }]);
return local;
} catch (e) {
this._onDidInstallMcpServers.fire([{ name: server.name, source: server, error: e, mcpResource }]);
throw e;
}
}
protected async getLocalMcpServerInfo(scannedMcpServer: IScannedMcpServer): Promise<ILocalMcpServerInfo | undefined> {
let storedMcpServerInfo: ILocalMcpServerInfo | undefined;
let location: URI | undefined;
let readmeUrl: URI | undefined;
if (scannedMcpServer.gallery) {
location = this.getLocation(scannedMcpServer.name, scannedMcpServer.version);
const manifestLocation = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
try {
const content = await this.fileService.readFile(manifestLocation);
storedMcpServerInfo = JSON.parse(content.value.toString()) as ILocalMcpServerInfo;
storedMcpServerInfo.location = location;
readmeUrl = this.uriIdentityService.extUri.joinPath(location, 'README.md');
if (!await this.fileService.exists(readmeUrl)) {
readmeUrl = undefined;
}
storedMcpServerInfo.readmeUrl = readmeUrl;
} catch (e) {
this.logService.error('MCP Management Service: failed to read manifest', location.toString(), e);
}
}
return storedMcpServerInfo;
}
protected getDefaultMcpResource(): URI {
return this.userDataProfilesService.defaultProfile.mcpResource;
}
private getLocation(name: string, version: string): URI {
return this.uriIdentityService.extUri.joinPath(this.mcpLocation, `${name.replace('/', '.')}-${version}`);
}
}

View File

@ -4,16 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { IStringDictionary } from '../../../base/common/collections.js';
import { UriComponents } from '../../../base/common/uri.js';
export interface IMcpConfiguration {
inputs?: unknown[];
/** @deprecated Only for rough cross-compat with other formats */
mcpServers?: Record<string, IMcpConfigurationStdio>;
servers?: Record<string, IMcpConfigurationStdio | IMcpConfigurationHTTP>;
}
export type McpConfigurationServer = IMcpConfigurationStdio | IMcpConfigurationHTTP;
export interface IMcpDevModeConfig {
/** Pattern or list of glob patterns to watch relative to the workspace folder. */
@ -22,25 +12,6 @@ export interface IMcpDevModeConfig {
debug?: { type: 'node' } | { type: 'debugpy'; debugpyPath?: string };
}
export interface IMcpConfigurationCommon {
dev?: IMcpDevModeConfig;
}
export interface IMcpConfigurationStdio extends IMcpConfigurationCommon {
type?: 'stdio';
command: string;
args?: readonly string[];
env?: Record<string, string | number | null>;
envFile?: string;
cwd?: string;
}
export interface IMcpConfigurationHTTP extends IMcpConfigurationCommon {
type?: 'http';
url: string;
headers?: Record<string, string>;
}
export const enum McpServerVariableType {
PROMPT = 'promptString',
PICK = 'pickString',
@ -56,24 +27,25 @@ export interface IMcpServerVariable {
readonly serverName?: string;
}
export interface IMcpServerConfiguration {
readonly location?: UriComponents;
}
export interface IMcpStdioServerConfiguration extends IMcpServerConfiguration {
export interface IMcpStdioServerConfiguration {
readonly type: 'stdio';
readonly command: string;
readonly args?: readonly string[];
readonly env?: Record<string, string | number | null>;
readonly envFile?: string;
readonly cwd?: string;
readonly dev?: IMcpDevModeConfig;
}
export interface IMcpRemtoeServerConfiguration extends IMcpServerConfiguration {
export interface IMcpRemoteServerConfiguration {
readonly type: 'http';
readonly url: string;
readonly headers?: Record<string, string>;
readonly dev?: IMcpDevModeConfig;
}
export type IMcpServerConfiguration = IMcpStdioServerConfiguration | IMcpRemoteServerConfiguration;
export interface IMcpServersConfiguration {
servers?: IStringDictionary<IMcpServerConfiguration>;
inputs?: IMcpServerVariable[];

View File

@ -0,0 +1,227 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Queue } from '../../../base/common/async.js';
import { VSBuffer } from '../../../base/common/buffer.js';
import { IStringDictionary } from '../../../base/common/collections.js';
import { parse, ParseError } from '../../../base/common/json.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../base/common/map.js';
import { URI } from '../../../base/common/uri.js';
import { ConfigurationTarget, ConfigurationTargetToString } from '../../configuration/common/configuration.js';
import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IScannedMcpServers, IScannedMcpServer } from './mcpManagement.js';
import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js';
interface IScannedWorkspaceFolderMcpServers {
servers?: IStringDictionary<IMcpServerConfiguration>;
inputs?: IMcpServerVariable[];
}
interface IScannedWorkspaceMcpServers {
settings?: {
mcp?: IScannedWorkspaceFolderMcpServers;
};
}
export interface ProfileMcpServersEvent {
readonly servers: readonly IScannedMcpServer[];
readonly profileLocation: URI;
}
export interface DidAddProfileMcpServersEvent extends ProfileMcpServersEvent {
readonly error?: Error;
}
export interface DidRemoveProfileMcpServersEvent extends ProfileMcpServersEvent {
readonly error?: Error;
}
export type McpResourceTarget = ConfigurationTarget.USER | ConfigurationTarget.WORKSPACE | ConfigurationTarget.WORKSPACE_FOLDER;
export const IMcpResourceScannerService = createDecorator<IMcpResourceScannerService>('IMcpResourceScannerService');
export interface IMcpResourceScannerService {
readonly _serviceBrand: undefined;
scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers>;
addMcpServers(servers: { server: IScannedMcpServer; inputs?: IMcpServerVariable[] }[], mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServer[]>;
removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void>;
}
export class McpResourceScannerService extends Disposable implements IMcpResourceScannerService {
readonly _serviceBrand: undefined;
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<any>>();
constructor(
@IFileService private readonly fileService: IFileService,
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
) {
super();
}
async scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers> {
return this.withProfileMcpServers(mcpResource, target);
}
async addMcpServers(servers: { server: IScannedMcpServer; inputs?: IMcpServerVariable[] }[], mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServer[]> {
const result: IScannedMcpServer[] = [];
await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {
let updatedInputs = scannedMcpServers.inputs ?? [];
const existingServers = scannedMcpServers.servers ?? {};
for (const { server, inputs } of servers) {
existingServers[server.name] = server;
result.push(server);
if (inputs) {
const existingInputIds = new Set(updatedInputs.map(input => input.id));
const newInputs = inputs.filter(input => !existingInputIds.has(input.id));
updatedInputs = [...updatedInputs, ...newInputs];
}
}
return { servers: existingServers, inputs: updatedInputs };
});
return result;
}
async removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void> {
await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {
for (const serverName of serverNames) {
if (scannedMcpServers.servers?.[serverName]) {
delete scannedMcpServers.servers[serverName];
}
}
return scannedMcpServers;
});
}
private async withProfileMcpServers(mcpResource: URI, target?: McpResourceTarget, updateFn?: (data: IScannedMcpServers) => IScannedMcpServers): Promise<IScannedMcpServers> {
return this.getResourceAccessQueue(mcpResource)
.queue(async () => {
target = target ?? ConfigurationTarget.USER;
let scannedMcpServers: IScannedMcpServers | undefined;
try {
const content = await this.fileService.readFile(mcpResource);
const errors: ParseError[] = [];
const result = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true });
if (errors.length > 0) {
throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));
}
if (target === ConfigurationTarget.USER) {
scannedMcpServers = result;
} else if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
scannedMcpServers = this.fromWorkspaceFolderMcpServers(result);
} else if (target === ConfigurationTarget.WORKSPACE) {
const workspaceScannedMcpServers: IScannedWorkspaceMcpServers = result;
if (workspaceScannedMcpServers.settings?.mcp) {
scannedMcpServers = this.fromWorkspaceFolderMcpServers(workspaceScannedMcpServers.settings?.mcp);
}
}
} catch (error) {
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
throw error;
}
}
if (updateFn) {
scannedMcpServers = updateFn(scannedMcpServers ?? {});
if (target === ConfigurationTarget.USER) {
return this.writeScannedMcpServers(mcpResource, scannedMcpServers);
}
if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
return this.writeScannedMcpServersToWorkspaceFolder(mcpResource, scannedMcpServers);
}
if (target === ConfigurationTarget.WORKSPACE) {
return this.writeScannedMcpServersToWorkspace(mcpResource, scannedMcpServers);
}
throw new Error(`Invalid Target: ${ConfigurationTargetToString(target)}`);
}
return scannedMcpServers;
});
}
private async writeScannedMcpServers(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
if ((scannedMcpServers.servers && Object.keys(scannedMcpServers.servers).length > 0) || (scannedMcpServers.inputs && scannedMcpServers.inputs.length > 0)) {
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedMcpServers, null, '\t')));
} else {
await this.fileService.del(mcpResource);
}
}
private async writeScannedMcpServersToWorkspaceFolder(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(this.toWorkspaceFolderMcpServers(scannedMcpServers), null, '\t')));
}
private async writeScannedMcpServersToWorkspace(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
let scannedWorkspaceMcpServers: IScannedWorkspaceMcpServers | undefined;
try {
const content = await this.fileService.readFile(mcpResource);
const errors: ParseError[] = [];
scannedWorkspaceMcpServers = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true }) as IScannedWorkspaceMcpServers;
if (errors.length > 0) {
throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));
}
} catch (error) {
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
throw error;
}
scannedWorkspaceMcpServers = { settings: {} };
}
if (!scannedWorkspaceMcpServers.settings) {
scannedWorkspaceMcpServers.settings = {};
}
scannedWorkspaceMcpServers.settings.mcp = this.toWorkspaceFolderMcpServers(scannedMcpServers);
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedWorkspaceMcpServers, null, '\t')));
}
private fromWorkspaceFolderMcpServers(scannedWorkspaceFolderMcpServers: IScannedWorkspaceFolderMcpServers): IScannedMcpServers {
const scannedMcpServers: IScannedMcpServers = {
inputs: scannedWorkspaceFolderMcpServers.inputs
};
const servers = Object.entries(scannedWorkspaceFolderMcpServers.servers ?? {});
if (servers.length > 0) {
scannedMcpServers.servers = {};
for (const [serverName, config] of servers) {
scannedMcpServers.servers[serverName] = {
id: serverName,
name: serverName,
version: '0.0.1',
config
};
}
}
return scannedMcpServers;
}
private toWorkspaceFolderMcpServers(scannedMcpServers: IScannedMcpServers): IScannedWorkspaceFolderMcpServers {
const scannedWorkspaceFolderMcpServers: IScannedWorkspaceFolderMcpServers = {};
if (scannedMcpServers.inputs) {
scannedWorkspaceFolderMcpServers.inputs = scannedMcpServers.inputs;
}
const servers = Object.entries(scannedMcpServers.servers ?? {});
if (servers.length > 0) {
scannedWorkspaceFolderMcpServers.servers = {};
for (const [serverName, server] of servers) {
scannedWorkspaceFolderMcpServers.servers[serverName] = server.config;
}
}
return scannedWorkspaceFolderMcpServers;
}
private getResourceAccessQueue(file: URI): Queue<any> {
let resourceQueue = this.resourcesAccessQueueMap.get(file);
if (!resourceQueue) {
resourceQueue = new Queue<any>();
this.resourcesAccessQueueMap.set(file, resourceQueue);
}
return resourceQueue;
}
}
registerSingleton(IMcpResourceScannerService, McpResourceScannerService, InstantiationType.Delayed);

View File

@ -13,6 +13,7 @@ export interface IRemoteAgentEnvironment {
connectionToken: string;
appRoot: URI;
settingsPath: URI;
mcpResource: URI;
logsPath: URI;
extensionHostLogsPath: URI;
globalStorageHome: URI;

View File

@ -43,6 +43,7 @@ suite('StorageMainService', function () {
settingsResource: joinPath(inMemoryProfileRoot, 'settingsResource'),
keybindingsResource: joinPath(inMemoryProfileRoot, 'keybindingsResource'),
tasksResource: joinPath(inMemoryProfileRoot, 'tasksResource'),
mcpResource: joinPath(inMemoryProfileRoot, 'mcp.json'),
snippetsHome: joinPath(inMemoryProfileRoot, 'snippetsHome'),
promptsHome: joinPath(inMemoryProfileRoot, 'promptsHome'),
extensionsResource: joinPath(inMemoryProfileRoot, 'extensionsResource'),

View File

@ -29,6 +29,7 @@ export const enum ProfileResourceType {
Tasks = 'tasks',
Extensions = 'extensions',
GlobalState = 'globalState',
Mcp = 'mcp',
}
/**
@ -50,6 +51,7 @@ export interface IUserDataProfile {
readonly snippetsHome: URI;
readonly promptsHome: URI;
readonly extensionsResource: URI;
readonly mcpResource: URI;
readonly cacheHome: URI;
readonly useDefaultFlags?: UseDefaultProfileFlags;
readonly isTransient?: boolean;
@ -71,6 +73,7 @@ export function isUserDataProfile(thing: unknown): thing is IUserDataProfile {
&& URI.isUri(candidate.snippetsHome)
&& URI.isUri(candidate.promptsHome)
&& URI.isUri(candidate.extensionsResource)
&& URI.isUri(candidate.mcpResource)
);
}
@ -137,6 +140,7 @@ export function reviveProfile(profile: UriDto<IUserDataProfile>, scheme: string)
snippetsHome: URI.revive(profile.snippetsHome).with({ scheme }),
promptsHome: URI.revive(profile.promptsHome).with({ scheme }),
extensionsResource: URI.revive(profile.extensionsResource).with({ scheme }),
mcpResource: URI.revive(profile.mcpResource).with({ scheme }),
cacheHome: URI.revive(profile.cacheHome).with({ scheme }),
useDefaultFlags: profile.useDefaultFlags,
isTransient: profile.isTransient,
@ -158,6 +162,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, profi
snippetsHome: defaultProfile && options?.useDefaultFlags?.snippets ? defaultProfile.snippetsHome : joinPath(location, 'snippets'),
promptsHome: defaultProfile && options?.useDefaultFlags?.prompts ? defaultProfile.promptsHome : joinPath(location, 'prompts'),
extensionsResource: defaultProfile && options?.useDefaultFlags?.extensions ? defaultProfile.extensionsResource : joinPath(location, 'extensions.json'),
mcpResource: defaultProfile && options?.useDefaultFlags?.mcp ? defaultProfile.mcpResource : joinPath(location, 'mcp.json'),
cacheHome: joinPath(profilesCacheHome, id),
useDefaultFlags: options?.useDefaultFlags,
isTransient: options?.transient,

View File

@ -0,0 +1,271 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../base/common/cancellation.js';
import { URI } from '../../../base/common/uri.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
import { IStorageService } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js';
import { AbstractFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from './abstractSynchronizer.js';
import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, USER_DATA_SYNC_SCHEME, SyncResource } from './userDataSync.js';
export interface IJsonResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
export abstract class AbstractJsonSynchronizer extends AbstractFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
private readonly previewResource: URI;
private readonly baseResource: URI;
private readonly localResource: URI;
private readonly remoteResource: URI;
private readonly acceptedResource: URI;
constructor(
fileResource: URI,
syncResourceMetadata: { syncResource: SyncResource; profile: IUserDataProfile },
collection: string | undefined,
previewFileName: string,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
) {
super(fileResource, syncResourceMetadata, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
this.previewResource = this.extUri.joinPath(this.syncPreviewFolder, previewFileName);
this.baseResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
this.localResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
this.remoteResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
this.acceptedResource = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
}
protected abstract getContentFromSyncContent(syncContent: string): string | null;
protected abstract toSyncContent(content: string | null): object;
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise<IJsonResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? this.getContentFromSyncContent(remoteUserData.syncData.content) : null;
// Use remote data as last sync data if last sync data does not exist and remote data is from same machine
lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData;
const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteUserData.syncData) {
const localContent = fileContent ? fileContent.value.toString() : null;
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ${this.syncResource.syncResource} with local ${this.syncResource.syncResource}...`);
const result = this.merge(localContent, remoteContent, lastSyncContent);
content = result.content;
hasConflicts = result.hasConflicts;
hasLocalChanged = result.hasLocalChanged;
hasRemoteChanged = result.hasRemoteChanged;
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote ${this.syncResource.syncResource} does not exist. Synchronizing ${this.syncResource.syncResource} for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
const previewResult: IMergeResult = {
content: hasConflicts ? lastSyncContent : content,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts
};
const localContent = fileContent ? fileContent.value.toString() : null;
return [{
fileContent,
baseResource: this.baseResource,
baseContent: lastSyncContent,
localResource: this.localResource,
localContent,
localChange: previewResult.localChange,
remoteResource: this.remoteResource,
remoteContent,
remoteChange: previewResult.remoteChange,
previewResource: this.previewResource,
previewResult,
acceptedResource: this.acceptedResource,
}];
}
protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise<boolean> {
const lastSyncContent: string | null = lastSyncUserData?.syncData ? this.getContentFromSyncContent(lastSyncUserData.syncData.content) : null;
if (lastSyncContent === null) {
return true;
}
const fileContent = await this.getLocalFileContent();
const localContent = fileContent ? fileContent.value.toString() : null;
const result = this.merge(localContent, lastSyncContent, lastSyncContent);
return result.hasLocalChanged || result.hasRemoteChanged;
}
protected async getMergeResult(resourcePreview: IJsonResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return resourcePreview.previewResult;
}
protected async getAcceptResult(resourcePreview: IJsonResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
/* Accept local resource */
if (this.extUri.isEqual(resource, this.localResource)) {
return {
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
/* Accept remote resource */
if (this.extUri.isEqual(resource, this.remoteResource)) {
return {
content: resourcePreview.remoteContent,
localChange: Change.Modified,
remoteChange: Change.None,
};
}
/* Accept preview resource */
if (this.extUri.isEqual(resource, this.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
return {
content,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IJsonResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const { fileContent } = resourcePreviews[0][0];
const { content, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ${this.syncResource.syncResource}.`);
}
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ${this.syncResource.syncResource}...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSyncContent(fileContent.value.toString())));
}
if (content) {
await this.updateLocalFileContent(content, fileContent, force);
} else {
await this.deleteLocalFile();
}
this.logService.info(`${this.syncResourceLogLabel}: Updated local ${this.syncResource.syncResource}`);
}
if (remoteChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ${this.syncResource.syncResource}...`);
const remoteContents = JSON.stringify(this.toSyncContent(content));
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ${this.syncResource.syncResource}`);
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ${this.syncResource.syncResource}...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ${this.syncResource.syncResource}`);
}
}
async hasLocalData(): Promise<boolean> {
return this.fileService.exists(this.file);
}
async resolveContent(uri: URI): Promise<string | null> {
if (this.extUri.isEqual(this.remoteResource, uri)
|| this.extUri.isEqual(this.baseResource, uri)
|| this.extUri.isEqual(this.localResource, uri)
|| this.extUri.isEqual(this.acceptedResource, uri)
) {
return this.resolvePreviewContent(uri);
}
return null;
}
private merge(originalLocalContent: string | null, originalRemoteContent: string | null, baseContent: string | null): {
content: string | null;
hasLocalChanged: boolean;
hasRemoteChanged: boolean;
hasConflicts: boolean;
} {
/* no changes */
if (originalLocalContent === null && originalRemoteContent === null && baseContent === null) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
/* no changes */
if (originalLocalContent === originalRemoteContent) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
const localForwarded = baseContent !== originalLocalContent;
const remoteForwarded = baseContent !== originalRemoteContent;
/* no changes */
if (!localForwarded && !remoteForwarded) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
/* local has changed and remote has not */
if (localForwarded && !remoteForwarded) {
return { content: originalLocalContent, hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false };
}
/* remote has changed and local has not */
if (remoteForwarded && !localForwarded) {
return { content: originalRemoteContent, hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false };
}
return { content: originalLocalContent, hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: true };
}
}

View File

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { IStorageService } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfile } from '../../userDataProfile/common/userDataProfile.js';
import { AbstractJsonSynchronizer } from './abstractJsonSynchronizer.js';
import { IUserDataSyncLocalStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource } from './userDataSync.js';
interface IMcpSyncContent {
mcp?: string;
}
export function getMcpContentFromSyncContent(syncContent: string, logService: ILogService): string | null {
try {
const parsed = <IMcpSyncContent>JSON.parse(syncContent);
return parsed.mcp ?? null;
} catch (e) {
logService.error(e);
return null;
}
}
export class McpSynchroniser extends AbstractJsonSynchronizer implements IUserDataSynchroniser {
constructor(
profile: IUserDataProfile,
collection: string | undefined,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@ITelemetryService telemetryService: ITelemetryService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
) {
super(profile.mcpResource, { syncResource: SyncResource.Mcp, profile }, collection, 'mcp.json', fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
}
protected getContentFromSyncContent(syncContent: string): string | null {
return getMcpContentFromSyncContent(syncContent, this.logService);
}
protected toSyncContent(mcp: string | null): IMcpSyncContent {
return mcp ? { mcp } : {};
}
}

View File

@ -4,8 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../base/common/buffer.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { URI } from '../../../base/common/uri.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
@ -14,17 +12,14 @@ import { IStorageService } from '../../storage/common/storage.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
import { AbstractFileSynchroniser, AbstractInitializer, IAcceptResult, IFileResourcePreview, IMergeResult } from './abstractSynchronizer.js';
import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from './userDataSync.js';
import { AbstractJsonSynchronizer } from './abstractJsonSynchronizer.js';
import { AbstractInitializer } from './abstractSynchronizer.js';
import { IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource } from './userDataSync.js';
interface ITasksSyncContent {
tasks?: string;
}
interface ITasksResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
export function getTasksContentFromSyncContent(syncContent: string, logService: ILogService): string | null {
try {
const parsed = <ITasksSyncContent>JSON.parse(syncContent);
@ -35,14 +30,7 @@ export function getTasksContentFromSyncContent(syncContent: string, logService:
}
}
export class TasksSynchroniser extends AbstractFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'tasks.json');
private readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
export class TasksSynchroniser extends AbstractJsonSynchronizer implements IUserDataSynchroniser {
constructor(
profile: IUserDataProfile,
@ -58,191 +46,16 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser
@ITelemetryService telemetryService: ITelemetryService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
) {
super(profile.tasksResource, { syncResource: SyncResource.Tasks, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
super(profile.tasksResource, { syncResource: SyncResource.Tasks, profile }, collection, 'tasks.json', fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise<ITasksResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? getTasksContentFromSyncContent(remoteUserData.syncData.content, this.logService) : null;
// Use remote data as last sync data if last sync data does not exist and remote data is from same machine
lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData;
const lastSyncContent: string | null = lastSyncUserData?.syncData ? getTasksContentFromSyncContent(lastSyncUserData.syncData.content, this.logService) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteUserData.syncData) {
const localContent = fileContent ? fileContent.value.toString() : null;
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote tasks with local tasks...`);
const result = merge(localContent, remoteContent, lastSyncContent);
content = result.content;
hasConflicts = result.hasConflicts;
hasLocalChanged = result.hasLocalChanged;
hasRemoteChanged = result.hasRemoteChanged;
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote tasks does not exist. Synchronizing tasks for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
const previewResult: IMergeResult = {
content: hasConflicts ? lastSyncContent : content,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts
};
const localContent = fileContent ? fileContent.value.toString() : null;
return [{
fileContent,
baseResource: this.baseResource,
baseContent: lastSyncContent,
localResource: this.localResource,
localContent,
localChange: previewResult.localChange,
remoteResource: this.remoteResource,
remoteContent,
remoteChange: previewResult.remoteChange,
previewResource: this.previewResource,
previewResult,
acceptedResource: this.acceptedResource,
}];
protected getContentFromSyncContent(syncContent: string): string | null {
return getTasksContentFromSyncContent(syncContent, this.logService);
}
protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise<boolean> {
const lastSyncContent: string | null = lastSyncUserData?.syncData ? getTasksContentFromSyncContent(lastSyncUserData.syncData.content, this.logService) : null;
if (lastSyncContent === null) {
return true;
}
const fileContent = await this.getLocalFileContent();
const localContent = fileContent ? fileContent.value.toString() : null;
const result = merge(localContent, lastSyncContent, lastSyncContent);
return result.hasLocalChanged || result.hasRemoteChanged;
}
protected async getMergeResult(resourcePreview: ITasksResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return resourcePreview.previewResult;
}
protected async getAcceptResult(resourcePreview: ITasksResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
/* Accept local resource */
if (this.extUri.isEqual(resource, this.localResource)) {
return {
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
/* Accept remote resource */
if (this.extUri.isEqual(resource, this.remoteResource)) {
return {
content: resourcePreview.remoteContent,
localChange: Change.Modified,
remoteChange: Change.None,
};
}
/* Accept preview resource */
if (this.extUri.isEqual(resource, this.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
return {
content,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ITasksResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const { fileContent } = resourcePreviews[0][0];
const { content, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing tasks.`);
}
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local tasks...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toTasksSyncContent(fileContent.value.toString())));
}
if (content) {
await this.updateLocalFileContent(content, fileContent, force);
} else {
await this.deleteLocalFile();
}
this.logService.info(`${this.syncResourceLogLabel}: Updated local tasks`);
}
if (remoteChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote tasks...`);
const remoteContents = JSON.stringify(this.toTasksSyncContent(content));
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote tasks`);
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized tasks...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized tasks`);
}
}
async hasLocalData(): Promise<boolean> {
return this.fileService.exists(this.file);
}
override async resolveContent(uri: URI): Promise<string | null> {
if (this.extUri.isEqual(this.remoteResource, uri)
|| this.extUri.isEqual(this.baseResource, uri)
|| this.extUri.isEqual(this.localResource, uri)
|| this.extUri.isEqual(this.acceptedResource, uri)
) {
return this.resolvePreviewContent(uri);
}
return null;
}
private toTasksSyncContent(tasks: string | null): ITasksSyncContent {
protected toSyncContent(tasks: string | null): ITasksSyncContent {
return tasks ? { tasks } : {};
}
}
export class TasksInitializer extends AbstractInitializer {
@ -283,41 +96,3 @@ export class TasksInitializer extends AbstractInitializer {
}
}
function merge(originalLocalContent: string | null, originalRemoteContent: string | null, baseContent: string | null): {
content: string | null;
hasLocalChanged: boolean;
hasRemoteChanged: boolean;
hasConflicts: boolean;
} {
/* no changes */
if (originalLocalContent === null && originalRemoteContent === null && baseContent === null) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
/* no changes */
if (originalLocalContent === originalRemoteContent) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
const localForwarded = baseContent !== originalLocalContent;
const remoteForwarded = baseContent !== originalRemoteContent;
/* no changes */
if (!localForwarded && !remoteForwarded) {
return { content: null, hasLocalChanged: false, hasRemoteChanged: false, hasConflicts: false };
}
/* local has changed and remote has not */
if (localForwarded && !remoteForwarded) {
return { content: originalLocalContent, hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false };
}
/* remote has changed and local has not */
if (remoteForwarded && !localForwarded) {
return { content: originalRemoteContent, hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false };
}
return { content: originalLocalContent, hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: true };
}

View File

@ -169,12 +169,13 @@ export const enum SyncResource {
Snippets = 'snippets',
Prompts = 'prompts',
Tasks = 'tasks',
Mcp = 'mcp',
Extensions = 'extensions',
GlobalState = 'globalState',
Profiles = 'profiles',
WorkspaceState = 'workspaceState',
}
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Prompts, SyncResource.Tasks, SyncResource.Extensions, SyncResource.GlobalState, SyncResource.Profiles];
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Prompts, SyncResource.Tasks, SyncResource.Extensions, SyncResource.GlobalState, SyncResource.Profiles, SyncResource.Mcp];
export function getPathSegments(collection: string | undefined, ...paths: string[]): string[] {
return collection ? [collection, ...paths] : paths;

View File

@ -19,6 +19,7 @@ import { parseSettingsSyncContent } from './settingsSync.js';
import { getKeybindingsContentFromSyncContent } from './keybindingsSync.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { getTasksContentFromSyncContent } from './tasksSync.js';
import { getMcpContentFromSyncContent } from './mcpSync.js';
import { LocalExtensionsProvider, parseExtensions, stringify as stringifyExtensions } from './extensionsSync.js';
import { LocalGlobalStateProvider, stringify as stringifyGlobalState } from './globalStateSync.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
@ -145,6 +146,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
case SyncResource.Settings: return this.getSettingsAssociatedResources(uri, profile);
case SyncResource.Keybindings: return this.getKeybindingsAssociatedResources(uri, profile);
case SyncResource.Tasks: return this.getTasksAssociatedResources(uri, profile);
case SyncResource.Mcp: return this.getMcpAssociatedResources(uri, profile);
case SyncResource.Snippets: return this.getSnippetsAssociatedResources(uri, profile);
case SyncResource.Prompts: return this.getPromptsAssociatedResources(uri, profile);
case SyncResource.GlobalState: return this.getGlobalStateAssociatedResources(uri, profile);
@ -223,6 +225,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
case SyncResource.Settings: return this.resolveSettingsNodeContent(syncData, node);
case SyncResource.Keybindings: return this.resolveKeybindingsNodeContent(syncData, node);
case SyncResource.Tasks: return this.resolveTasksNodeContent(syncData, node);
case SyncResource.Mcp: return this.resolveMcpNodeContent(syncData, node);
case SyncResource.Snippets: return this.resolveSnippetsNodeContent(syncData, node);
case SyncResource.Prompts: return this.resolvePromptsNodeContent(syncData, node);
case SyncResource.GlobalState: return this.resolveGlobalStateNodeContent(syncData, node);
@ -244,6 +247,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
case SyncResource.Settings: return null;
case SyncResource.Keybindings: return null;
case SyncResource.Tasks: return null;
case SyncResource.Mcp: return null;
case SyncResource.Snippets: return null;
case SyncResource.Prompts: return null;
case SyncResource.WorkspaceState: return null;
@ -513,4 +517,18 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
return { ref, content };
}
private getMcpAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] {
const resource = this.extUri.joinPath(uri, 'mcp.json');
const comparableResource = profile ? profile.mcpResource : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE);
return [{ resource, comparableResource }];
}
private resolveMcpNodeContent(syncData: ISyncData, node: string): string | null {
switch (node) {
case 'mcp.json':
return getMcpContentFromSyncContent(syncData.content, this.logService);
}
return null;
}
}

View File

@ -27,6 +27,7 @@ import { PromptsSynchronizer } from './promptsSync/promptsSync.js';
import { SettingsSynchroniser } from './settingsSync.js';
import { SnippetsSynchroniser } from './snippetsSync.js';
import { TasksSynchroniser } from './tasksSync.js';
import { McpSynchroniser } from './mcpSync.js';
import { UserDataProfilesManifestSynchroniser } from './userDataProfilesManifestSync.js';
import {
ALL_SYNC_RESOURCES, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError,
@ -711,6 +712,7 @@ class ProfileSynchronizer extends Disposable {
case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser, this.profile, this.collection);
case SyncResource.Prompts: return this.instantiationService.createInstance(PromptsSynchronizer, this.profile, this.collection);
case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser, this.profile, this.collection);
case SyncResource.Mcp: return this.instantiationService.createInstance(McpSynchroniser, this.profile, this.collection);
case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser, this.profile, this.collection);
case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser, this.profile, this.collection);
case SyncResource.Profiles: return this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.profile, this.collection);
@ -863,11 +865,12 @@ class ProfileSynchronizer extends Disposable {
case SyncResource.Keybindings: return 1;
case SyncResource.Snippets: return 2;
case SyncResource.Tasks: return 3;
case SyncResource.GlobalState: return 4;
case SyncResource.Extensions: return 5;
case SyncResource.Prompts: return 6;
case SyncResource.Profiles: return 7;
case SyncResource.WorkspaceState: return 8;
case SyncResource.Mcp: return 4;
case SyncResource.GlobalState: return 5;
case SyncResource.Extensions: return 6;
case SyncResource.Prompts: return 7;
case SyncResource.Profiles: return 8;
case SyncResource.WorkspaceState: return 9;
}
}
}

View File

@ -0,0 +1,577 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { runWithFakedTimers } from '../../../../base/test/common/timeTravelScheduler.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { IFileService } from '../../../files/common/files.js';
import { ILogService } from '../../../log/common/log.js';
import { IUserDataProfilesService } from '../../../userDataProfile/common/userDataProfile.js';
import { getMcpContentFromSyncContent, McpSynchroniser } from '../../common/mcpSync.js';
import { Change, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus } from '../../common/userDataSync.js';
import { UserDataSyncClient, UserDataSyncTestServer } from './userDataSyncClient.js';
suite('McpSync', () => {
const server = new UserDataSyncTestServer();
let client: UserDataSyncClient;
let testObject: McpSynchroniser;
teardown(async () => {
await client.instantiationService.get(IUserDataSyncStoreService).clear();
});
const disposableStore = ensureNoDisposablesAreLeakedInTestSuite();
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp(true);
testObject = client.getSynchronizer(SyncResource.Mcp) as McpSynchroniser;
});
test('when mcp file does not exist', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
assert.deepStrictEqual(await testObject.getLastSyncUserData(), null);
let manifest = await client.getResourceManifest();
server.reset();
await testObject.sync(manifest);
assert.deepStrictEqual(server.requests, [
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
]);
assert.ok(!await fileService.exists(mcpResource));
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.strictEqual(lastSyncUserData!.syncData, null);
manifest = await client.getResourceManifest();
server.reset();
await testObject.sync(manifest);
assert.deepStrictEqual(server.requests, []);
manifest = await client.getResourceManifest();
server.reset();
await testObject.sync(manifest);
assert.deepStrictEqual(server.requests, []);
});
});
test('when mcp file does not exist and remote has changes', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.instantiationService.get(IFileService).writeFile(mcpResource2, VSBuffer.fromString(content));
await client2.sync();
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file exists locally and remote has no mcp', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
});
});
test('first time sync: when mcp file exists locally with same content as remote', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.instantiationService.get(IFileService).writeFile(mcpResource2, VSBuffer.fromString(content));
await client2.sync();
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file locally has moved forward', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
});
});
test('when mcp file remotely has moved forward', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
fileService2.writeFile(mcpResource2, VSBuffer.fromString(content));
await client2.sync();
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file has moved forward locally and remotely with same changes', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
fileService2.writeFile(mcpResource2, VSBuffer.fromString(content));
await client2.sync();
fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file has moved forward locally and remotely - accept preview', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {
'server1': {
'command': 'node',
'args': ['./server1.js']
}
}
})));
await client2.sync();
const content = JSON.stringify({
'mcpServers': {
'server2': {
'command': 'node',
'args': ['./server2.js']
}
}
});
fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
const previewContent = (await fileService.readFile(testObject.conflicts.conflicts[0].previewResource)).value.toString();
assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts);
assert.deepStrictEqual(testObject.conflicts.conflicts.length, 1);
assert.deepStrictEqual(testObject.conflicts.conflicts[0].mergeState, MergeState.Conflict);
assert.deepStrictEqual(testObject.conflicts.conflicts[0].localChange, Change.Modified);
assert.deepStrictEqual(testObject.conflicts.conflicts[0].remoteChange, Change.Modified);
await testObject.accept(testObject.conflicts.conflicts[0].previewResource);
await testObject.apply(false);
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), previewContent);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), previewContent);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), previewContent);
});
});
test('when mcp file has moved forward locally and remotely - accept modified preview', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {
'server1': {
'command': 'node',
'args': ['./server1.js']
}
}
})));
await client2.sync();
fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({
'mcpServers': {
'server2': {
'command': 'node',
'args': ['./server2.js']
}
}
})));
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'server1': {
'command': 'node',
'args': ['./server1.js']
},
'server2': {
'command': 'node',
'args': ['./server2.js']
}
}
});
await testObject.accept(testObject.conflicts.conflicts[0].previewResource, content);
await testObject.apply(false);
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file has moved forward locally and remotely - accept remote', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'server1': {
'command': 'node',
'args': ['./server1.js']
}
}
});
fileService2.writeFile(mcpResource2, VSBuffer.fromString(content));
await client2.sync();
fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({
'mcpServers': {
'server2': {
'command': 'node',
'args': ['./server2.js']
}
}
})));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts.conflicts[0].remoteResource);
await testObject.apply(false);
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file has moved forward locally and remotely - accept local', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
await fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await client2.sync();
await testObject.sync(await client.getResourceManifest());
fileService2.writeFile(mcpResource2, VSBuffer.fromString(JSON.stringify({
'mcpServers': {
'server1': {
'command': 'node',
'args': ['./server1.js']
}
}
})));
await client2.sync();
const content = JSON.stringify({
'mcpServers': {
'server2': {
'command': 'node',
'args': ['./server2.js']
}
}
});
fileService.writeFile(mcpResource, VSBuffer.fromString(content));
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts.conflicts[0].localResource);
await testObject.apply(false);
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content);
assert.strictEqual((await fileService.readFile(mcpResource)).value.toString(), content);
});
});
test('when mcp file was removed in one client', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({
'mcpServers': {}
})));
await testObject.sync(await client.getResourceManifest());
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
await client2.sync();
const mcpResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
const fileService2 = client2.instantiationService.get(IFileService);
fileService2.del(mcpResource2);
await client2.sync();
await testObject.sync(await client.getResourceManifest());
assert.deepStrictEqual(testObject.status, SyncStatus.Idle);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), null);
assert.strictEqual(getMcpContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), null);
assert.strictEqual(await fileService.exists(mcpResource), false);
});
});
test('when mcp file is created after first sync', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
await testObject.sync(await client.getResourceManifest());
const content = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
await fileService.createFile(mcpResource, VSBuffer.fromString(content));
let lastSyncUserData = await testObject.getLastSyncUserData();
const manifest = await client.getResourceManifest();
server.reset();
await testObject.sync(manifest);
assert.deepStrictEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
]);
lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.strictEqual(getMcpContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content);
});
});
test('apply remote when mcp file does not exist', async () => {
await runWithFakedTimers<void>({}, async () => {
const fileService = client.instantiationService.get(IFileService);
const mcpResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.mcpResource;
if (await fileService.exists(mcpResource)) {
await fileService.del(mcpResource);
}
const preview = (await testObject.sync(await client.getResourceManifest(), true))!;
server.reset();
const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource);
await testObject.accept(preview.resourcePreviews[0].remoteResource, content);
await testObject.apply(false);
assert.deepStrictEqual(server.requests, []);
});
});
test('sync profile mcp', async () => {
await runWithFakedTimers<void>({}, async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const profile = await client2.instantiationService.get(IUserDataProfilesService).createNamedProfile('profile1');
const expected = JSON.stringify({
'mcpServers': {
'test-server': {
'command': 'node',
'args': ['./server.js']
}
}
});
await client2.instantiationService.get(IFileService).createFile(profile.mcpResource, VSBuffer.fromString(expected));
await client2.sync();
await client.sync();
const syncedProfile = client.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === profile.id)!;
const actual = (await client.instantiationService.get(IFileService).readFile(syncedProfile.mcpResource)).value.toString();
assert.strictEqual(actual, expected);
});
});
});

View File

@ -159,6 +159,8 @@ suite('UserDataAutoSyncService', () => {
// Tasks
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } },
// Mcp
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },

View File

@ -42,6 +42,8 @@ suite('UserDataSyncService', () => {
// Tasks
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } },
// MCP
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
@ -79,6 +81,8 @@ suite('UserDataSyncService', () => {
// Snippets
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } },
// MCP
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
@ -113,6 +117,8 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
// Tasks
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
// MCP
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions
@ -147,6 +153,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/prompts/latest`, headers: {} },
@ -189,6 +196,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/prompts/latest`, headers: {} },
@ -234,6 +242,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/prompts/latest`, headers: {} },
@ -245,6 +254,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/prompts/latest`, headers: {} },
@ -354,6 +364,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/prompts/latest`, headers: {} },
@ -492,6 +503,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/prompts/latest`, headers: {} },
@ -551,6 +563,8 @@ suite('UserDataSyncService', () => {
// Tasks
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } },
// MCP
{ type: 'GET', url: `${target.url}/v1/resource/mcp/latest`, headers: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
@ -579,7 +593,7 @@ suite('UserDataSyncService', () => {
await (await testObject.createSyncTask(null)).run();
disposable.dispose();
assert.deepStrictEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
assert.deepStrictEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
});
test('test sync conflicts status', async () => {
@ -755,6 +769,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/prompts/latest`, headers: {} },
@ -809,6 +824,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/mcp/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/prompts/latest`, headers: {} },

View File

@ -110,6 +110,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
connectionToken: (this._connectionToken.type !== ServerConnectionTokenType.None ? this._connectionToken.value : ''),
appRoot: URI.file(this._environmentService.appRoot),
settingsPath: this._environmentService.machineSettingsResource,
mcpResource: this._environmentService.mcpResource,
logsPath: this._environmentService.logsHome,
extensionHostLogsPath: joinPath(this._environmentService.logsHome, `exthost${RemoteAgentEnvironmentChannel._namePool++}`),
globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,

View File

@ -11,6 +11,8 @@ import { refineServiceDecorator } from '../../platform/instantiation/common/inst
import { IEnvironmentService, INativeEnvironmentService } from '../../platform/environment/common/environment.js';
import { memoize } from '../../base/common/decorators.js';
import { URI } from '../../base/common/uri.js';
import { joinPath } from '../../base/common/resources.js';
import { join } from '../../base/common/path.js';
export const serverOptions: OptionDescriptions<Required<ServerParsedArgs>> = {
@ -223,11 +225,17 @@ export interface ServerParsedArgs {
export const IServerEnvironmentService = refineServiceDecorator<IEnvironmentService, IServerEnvironmentService>(IEnvironmentService);
export interface IServerEnvironmentService extends INativeEnvironmentService {
readonly machineSettingsResource: URI;
readonly mcpResource: URI;
readonly args: ServerParsedArgs;
}
export class ServerEnvironmentService extends NativeEnvironmentService implements IServerEnvironmentService {
@memoize
override get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get mcpResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'User')), 'mcp.json'); }
override get args(): ServerParsedArgs { return super.args as ServerParsedArgs; }
}

View File

@ -85,6 +85,11 @@ import { NativeMcpDiscoveryHelperChannel } from '../../platform/mcp/node/nativeM
import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js';
import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { ExtensionGalleryManifestIPCService } from '../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js';
import { IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js';
import { McpManagementService } from '../../platform/mcp/common/mcpManagementService.js';
import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.js';
import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js';
import { McpManagementChannel } from '../../platform/mcp/common/mcpManagementIpc.js';
const eventPrefix = 'monacoworkbench';
@ -215,7 +220,12 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
const ptyHostService = instantiationService.createInstance(PtyHostService, ptyHostStarter);
services.set(IPtyService, ptyHostService);
services.set(IMcpResourceScannerService, new SyncDescriptor(McpResourceScannerService));
services.set(IMcpGalleryService, new SyncDescriptor(McpGalleryService));
services.set(IMcpManagementService, new SyncDescriptor(McpManagementService));
instantiationService.invokeFunction(accessor => {
const mcpManagementService = accessor.get(IMcpManagementService);
const extensionManagementService = accessor.get(INativeServerExtensionManagementService);
const extensionsScannerService = accessor.get(IExtensionsScannerService);
const extensionGalleryService = accessor.get(IExtensionGalleryService);
@ -241,6 +251,8 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority));
socketServer.registerChannel('extensions', channel);
socketServer.registerChannel('mcpManagement', new McpManagementChannel(mcpManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)));
// clean up extensions folder
remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp());

View File

@ -28,9 +28,8 @@ import { Extensions, IConfigurationMigrationRegistry } from '../../../common/con
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js';
import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';
import { mcpSchemaId } from '../../../services/configuration/common/configuration.js';
import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js';
import { allDiscoverySources, discoverySourceLabel, mcpConfigurationSection, mcpDiscoverySection, mcpEnabledSection, mcpSchemaExampleServers, mcpServerSamplingSection } from '../../mcp/common/mcpConfiguration.js';
import { allDiscoverySources, discoverySourceLabel, mcpDiscoverySection, mcpEnabledSection, mcpServerSamplingSection } from '../../mcp/common/mcpConfiguration.js';
import { ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js';
import { CodeMapperService, ICodeMapperService } from '../common/chatCodeMapperService.js';
import '../common/chatColors.js';
@ -291,15 +290,6 @@ configurationRegistry.registerConfiguration({
}
},
},
[mcpConfigurationSection]: {
type: 'object',
default: {
inputs: [],
servers: mcpSchemaExampleServers,
},
description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),
$ref: mcpSchemaId
},
[ChatConfiguration.UseFileStorage]: {
type: 'boolean',
description: nls.localize('chat.useFileStorage', "Enables storing chat sessions on disk instead of in the storage service. Enabling this does a one-time per-workspace migration of existing sessions to the new format."),

View File

@ -161,6 +161,7 @@ suite('Edit session sync', () => {
settingsResource: URI.file('settingsResource'),
keybindingsResource: URI.file('keybindingsResource'),
tasksResource: URI.file('tasksResource'),
mcpResource: URI.file('mcp.json'),
snippetsHome: URI.file('snippetsHome'),
promptsHome: URI.file('promptsHome'),
extensionsResource: URI.file('extensionsResource'),

View File

@ -69,7 +69,6 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { URI } from '../../../../base/common/uri.js';
import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js';
export const ExtensionsSortByContext = new RawContextKey<string>('extensionsSortByValue', '');
export const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false);
@ -529,7 +528,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@ICommandService private readonly commandService: ICommandService,
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
@ILogService logService: ILogService,
) {
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService, logService);
@ -822,7 +820,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
this.searchDeprecatedExtensionsContextKey.set(ExtensionsListView.isSearchDeprecatedExtensionsQuery(value));
this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value));
this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery);
this.searchMcpServersContextKey.set(this.mcpGalleryService.isEnabled() && !!value && /@mcp\s?.*/i.test(value));
this.searchMcpServersContextKey.set(!!value && /@mcp\s?.*/i.test(value));
this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery && !this.searchMcpServersContextKey.get());
this.sortByUpdateDateContextKey.set(ExtensionsListView.isSortUpdateDateQuery(value));
this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value));

View File

@ -18,13 +18,12 @@ import { IViewsRegistry, Extensions as ViewExtensions } from '../../../common/vi
import { mcpSchemaId } from '../../../services/configuration/common/configuration.js';
import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';
import { DefaultViewsContext, SearchMcpServersContext } from '../../extensions/common/extensions.js';
import { ConfigMcpDiscovery } from '../common/discovery/configMcpDiscovery.js';
import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js';
import { InstalledMcpServersDiscovery } from '../common/discovery/installedMcpServersDiscovery.js';
import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js';
import { RemoteNativeMpcDiscovery } from '../common/discovery/nativeMcpRemoteDiscovery.js';
import { CursorWorkspaceMcpDiscoveryAdapter } from '../common/discovery/workspaceMcpDiscoveryAdapter.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { IMcpConfigPathsService, McpConfigPathsService } from '../common/mcpConfigPathsService.js';
import { mcpServerSchema } from '../common/mcpConfiguration.js';
import { McpContextKeysController } from '../common/mcpContextKeys.js';
import { IMcpDevModeDebugging, McpDevModeDebugging } from '../common/mcpDevMode.js';
@ -33,12 +32,13 @@ import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { McpResourceFilesystem } from '../common/mcpResourceFilesystem.js';
import { McpSamplingService } from '../common/mcpSamplingService.js';
import { McpService } from '../common/mcpService.js';
import { HasInstalledMcpServersContext, IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { HasInstalledMcpServersContext, IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId } from '../common/mcpTypes.js';
import { McpAddContextContribution } from './mcpAddContextContribution.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { McpDiscovery } from './mcpDiscovery.js';
import { McpElicitationService } from './mcpElicitationService.js';
import { McpLanguageFeatures } from './mcpLanguageFeatures.js';
import { McpConfigMigrationContribution } from './mcpMigration.js';
import { McpResourceQuickAccess } from './mcpResourceQuickAccess.js';
import { McpServerEditor } from './mcpServerEditor.js';
import { McpServerEditorInput } from './mcpServerEditorInput.js';
@ -49,13 +49,12 @@ import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchSe
registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed);
registerSingleton(IMcpService, McpService, InstantiationType.Delayed);
registerSingleton(IMcpWorkbenchService, McpWorkbenchService, InstantiationType.Eager);
registerSingleton(IMcpConfigPathsService, McpConfigPathsService, InstantiationType.Delayed);
registerSingleton(IMcpDevModeDebugging, McpDevModeDebugging, InstantiationType.Delayed);
registerSingleton(IMcpSamplingService, McpSamplingService, InstantiationType.Delayed);
registerSingleton(IMcpElicitationService, McpElicitationService, InstantiationType.Delayed);
mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery));
mcpDiscoveryRegistry.register(new SyncDescriptor(ConfigMcpDiscovery));
mcpDiscoveryRegistry.register(new SyncDescriptor(InstalledMcpServersDiscovery));
mcpDiscoveryRegistry.register(new SyncDescriptor(ExtensionMcpDiscovery));
mcpDiscoveryRegistry.register(new SyncDescriptor(CursorWorkspaceMcpDiscoveryAdapter));
@ -86,6 +85,7 @@ registerAction2(McpStartPromptingServerCommand);
registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2('mcpAddContext', McpAddContextContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(MCPContextsInitialisation.ID, MCPContextsInitialisation, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(McpConfigMigrationContribution.ID, McpConfigMigrationContribution, WorkbenchPhase.Eventually);
const jsonRegistry = <jsonContributionRegistry.IJSONContributionRegistry>Registry.as(jsonContributionRegistry.Extensions.JSONContribution);
jsonRegistry.registerSchema(mcpSchemaId, mcpServerSchema);
@ -95,7 +95,7 @@ Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([
id: InstalledMcpServersViewId,
name: localize2('mcp-installed', "MCP Servers - Installed"),
ctorDescriptor: new SyncDescriptor(McpServersListView),
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext, McpServersGalleryEnabledContext),
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext),
weight: 40,
order: 4,
canToggleVisibility: true
@ -104,7 +104,7 @@ Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([
id: 'workbench.views.mcp.marketplace',
name: localize2('mcp', "MCP Servers"),
ctorDescriptor: new SyncDescriptor(McpServersListView),
when: ContextKeyExpr.and(SearchMcpServersContext, McpServersGalleryEnabledContext),
when: ContextKeyExpr.and(SearchMcpServersContext),
}
], VIEW_CONTAINER);

View File

@ -24,7 +24,6 @@ import { ConfigurationTarget } from '../../../../platform/configuration/common/c
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { ExtensionsLocalizedLabel } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
@ -47,7 +46,7 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { McpContextKeys } from '../common/mcpContextKeys.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpConnectionState, mcpPromptPrefix, McpServerCacheState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, LazyCollectionState, McpCapability, McpConnectionState, mcpPromptPrefix, McpServerCacheState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
@ -83,18 +82,6 @@ export class ListMcpServerCommand extends Action2 {
const mcpService = accessor.get(IMcpService);
const commandService = accessor.get(ICommandService);
const quickInput = accessor.get(IQuickInputService);
const mcpWorkbenchService = accessor.get(IMcpWorkbenchService);
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const viewsService = accessor.get(IViewsService);
const mcpGalleryService = accessor.get(IMcpGalleryService);
if (mcpGalleryService.isEnabled()) {
if (mcpWorkbenchService.local.length) {
return viewsService.openView(InstalledMcpServersViewId, true);
} else {
return extensionWorkbenchService.openSearch('@mcp');
}
}
type ItemType = { id: string } & IQuickPickItem;
@ -616,7 +603,10 @@ export class AddConfigurationAction extends Action2 {
}
async run(accessor: ServicesAccessor, configUri?: string): Promise<void> {
return accessor.get(IInstantiationService).createInstance(McpAddConfigurationCommand, configUri).run();
const instantiationService = accessor.get(IInstantiationService);
const workspaceService = accessor.get(IWorkspaceContextService);
const target = configUri ? workspaceService.getWorkspaceFolder(URI.parse(configUri)) : undefined;
return instantiationService.createInstance(McpAddConfigurationCommand, target ?? undefined).run();
}
}
@ -779,7 +769,6 @@ export class McpBrowseCommand extends Action2 {
when: McpServersGalleryEnabledContext,
}, {
id: extensionsFilterSubMenu,
when: McpServersGalleryEnabledContext,
group: '1_predefined',
order: 1,
}],

View File

@ -15,21 +15,19 @@ import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js';
import { IMcpConfiguration, IMcpConfigurationHTTP, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';
import { ConfiguredInput } from '../../../services/configurationResolver/common/configurationResolver.js';
import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { IMcpConfigurationStdio, mcpConfigurationSection, mcpStdioServerSchema } from '../common/mcpConfiguration.js';
import { mcpStdioServerSchema } from '../common/mcpConfiguration.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpService, McpConnectionState } from '../common/mcpTypes.js';
@ -100,10 +98,9 @@ type AddServerCompletedClassification = {
export class McpAddConfigurationCommand {
constructor(
private readonly _explicitConfigUri: string | undefined,
private readonly workspaceFolder: IWorkspaceFolder | undefined,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IJSONEditingService private readonly _jsonEditingService: IJSONEditingService,
@IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@ICommandService private readonly _commandService: ICommandService,
@ -114,7 +111,6 @@ export class McpAddConfigurationCommand {
@INotificationService private readonly _notificationService: INotificationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IMcpService private readonly _mcpService: IMcpService,
@IMcpGalleryService private readonly _mcpGalleryService: IMcpGalleryService,
@ILabelService private readonly _label: ILabelService,
) { }
@ -143,15 +139,13 @@ export class McpAddConfigurationCommand {
);
}
if (this._mcpGalleryService.isEnabled()) {
items.push(
{ type: 'separator' },
{
kind: 'browse',
label: localize('mcp.servers.browse', "Browse MCP Servers..."),
}
);
}
items.push(
{ type: 'separator' },
{
kind: 'browse',
label: localize('mcp.servers.browse', "Browse MCP Servers..."),
}
);
const result = await this._quickInputService.pick<{ kind: AddConfigurationType | 'browse' } & IQuickPickItem>(items, {
placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add"),
@ -165,7 +159,7 @@ export class McpAddConfigurationCommand {
return result?.kind;
}
private async getStdioConfig(): Promise<IMcpConfigurationStdio | undefined> {
private async getStdioConfig(): Promise<IMcpStdioServerConfiguration | undefined> {
const command = await this._quickInputService.input({
title: localize('mcp.command.title', "Enter Command"),
placeHolder: localize('mcp.command.placeholder', "Command to run (with optional arguments)"),
@ -190,7 +184,7 @@ export class McpAddConfigurationCommand {
};
}
private async getSSEConfig(): Promise<IMcpConfigurationHTTP | undefined> {
private async getSSEConfig(): Promise<IMcpRemoteServerConfiguration | undefined> {
const url = await this._quickInputService.input({
title: localize('mcp.url.title', "Enter Server URL"),
placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"),
@ -205,7 +199,7 @@ export class McpAddConfigurationCommand {
packageType: 'sse'
});
return { url };
return { url, type: 'http' };
}
private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise<string | undefined> {
@ -219,21 +213,23 @@ export class McpAddConfigurationCommand {
return id;
}
private async getConfigurationTarget(): Promise<ConfigurationTarget | undefined> {
const options: (IQuickPickItem & { target: ConfigurationTarget })[] = [
{ target: ConfigurationTarget.USER, label: localize('mcp.target.user', "User Settings"), description: localize('mcp.target.user.description', "Available in all workspaces, runs locally") }
private async getConfigurationTarget(): Promise<ConfigurationTarget | IWorkspaceFolder | undefined> {
const options: (IQuickPickItem & { target?: ConfigurationTarget | IWorkspaceFolder })[] = [
{ target: ConfigurationTarget.USER_LOCAL, label: localize('mcp.target.user', "Global"), description: localize('mcp.target.user.description', "Available in all workspaces, runs locally") }
];
const raLabel = this._environmentService.remoteAuthority && this._label.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);
if (raLabel) {
options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote Settings"), description: localize('mcp.target..remote.description', "Available on this remote machine, runs on {0}", raLabel) });
options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote"), description: localize('mcp.target..remote.description', "Available on this remote machine, runs on {0}", raLabel) });
}
if (this._workspaceService.getWorkspace().folders.length > 0) {
const workbenchState = this._workspaceService.getWorkbenchState();
if (workbenchState !== WorkbenchState.EMPTY) {
const target = workbenchState === WorkbenchState.FOLDER ? this._workspaceService.getWorkspace().folders[0] : ConfigurationTarget.WORKSPACE;
if (this._environmentService.remoteAuthority) {
options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace Settings"), description: localize('mcp.target.workspace.description.remote', "Available in this workspace, runs on {0}", raLabel) });
options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description.remote', "Available in this workspace, runs on {0}", raLabel) });
} else {
options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace Settings"), description: localize('mcp.target.workspace.description', "Available in this workspace, runs locally") });
options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description', "Available in this workspace, runs locally") });
}
}
@ -241,15 +237,14 @@ export class McpAddConfigurationCommand {
return options[0].target;
}
const targetPick = await this._quickInputService.pick(options, {
title: localize('mcp.target.title', "Choose where to save the configuration"),
title: localize('mcp.target.title', "Choose where to install the MCP server"),
});
return targetPick?.target;
}
private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record<string, string> } | undefined> {
private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; server: Omit<IMcpStdioServerConfiguration, 'type'>; inputs?: IMcpServerVariable[]; inputValues?: Record<string, string> } | undefined> {
const packageName = await this._quickInputService.input({
ignoreFocusLost: true,
title: assistedTypes[type].title,
@ -325,7 +320,7 @@ export class McpAddConfigurationCommand {
return undefined;
}
return await this._commandService.executeCommand<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record<string, string> }>(
return await this._commandService.executeCommand<{ name: string; server: Omit<IMcpStdioServerConfiguration, 'type'>; inputs?: IMcpServerVariable[]; inputValues?: Record<string, string> }>(
AddConfigurationCopilotCommand.StartFlow,
{
name: packageName,
@ -371,15 +366,6 @@ export class McpAddConfigurationCommand {
store.add(disposableTimeout(() => store.dispose(), 5000));
}
private writeToUserSetting(name: string, config: McpConfigurationServer, target: ConfigurationTarget, inputs?: ConfiguredInput[]) {
const settings: IMcpConfiguration = { ...getConfigValueInTarget(this._configurationService.inspect<IMcpConfiguration>(mcpConfigurationSection), target) };
settings.servers = { ...settings.servers, [name]: config };
if (inputs) {
settings.inputs = [...(settings.inputs || []), ...inputs];
}
return this._configurationService.updateValue(mcpConfigurationSection, settings, target);
}
public async run(): Promise<void> {
// Step 1: Choose server type
const serverType = await this.getServerType();
@ -388,22 +374,22 @@ export class McpAddConfigurationCommand {
}
// Step 2: Get server details based on type
let serverConfig: McpConfigurationServer | undefined;
let config: IMcpServerConfiguration | undefined;
let suggestedName: string | undefined;
let inputs: ConfiguredInput[] | undefined;
let inputs: IMcpServerVariable[] | undefined;
let inputValues: Record<string, string> | undefined;
switch (serverType) {
case AddConfigurationType.Stdio:
serverConfig = await this.getStdioConfig();
config = await this.getStdioConfig();
break;
case AddConfigurationType.HTTP:
serverConfig = await this.getSSEConfig();
config = await this.getSSEConfig();
break;
case AddConfigurationType.NpmPackage:
case AddConfigurationType.PipPackage:
case AddConfigurationType.DockerImage: {
const r = await this.getAssistedConfig(serverType);
serverConfig = r?.server;
config = r?.server ? { ...r.server, type: 'stdio' } : undefined;
suggestedName = r?.name;
inputs = r?.inputs;
inputValues = r?.inputValues;
@ -413,51 +399,30 @@ export class McpAddConfigurationCommand {
assertNever(serverType);
}
if (!serverConfig) {
if (!config) {
return;
}
// Step 3: Get server ID
const serverId = await this.getServerId(suggestedName);
if (!serverId) {
const name = await this.getServerId(suggestedName);
if (!name) {
return;
}
// Step 4: Choose configuration target if no configUri provided
let target: ConfigurationTarget | undefined;
const workspace = this._workspaceService.getWorkspace();
if (!this._explicitConfigUri) {
let target: ConfigurationTarget | IWorkspaceFolder | undefined = this.workspaceFolder;
if (!target) {
target = await this.getConfigurationTarget();
if (!target) {
return;
}
}
// Step 5: Update configuration
const writeToUriDirect = this._explicitConfigUri
? URI.parse(this._explicitConfigUri)
: target === ConfigurationTarget.WORKSPACE && workspace.folders.length === 1
? URI.joinPath(workspace.folders[0].uri, '.vscode', 'mcp.json')
: undefined;
if (writeToUriDirect) {
await this._jsonEditingService.write(writeToUriDirect, [
{
path: ['servers', serverId],
value: serverConfig
},
...(inputs || []).map(i => ({
path: ['inputs', -1],
value: i,
})),
], true);
} else {
await this.writeToUserSetting(serverId, serverConfig, target!, inputs);
}
await this._mcpManagementService.install({ name, config, inputs }, { target });
if (inputValues) {
for (const [key, value] of Object.entries(inputValues)) {
await this._mcpRegistry.setSavedInput(key, target ?? ConfigurationTarget.WORKSPACE, value);
await this._mcpRegistry.setSavedInput(key, (isWorkspaceFolder(target) ? ConfigurationTarget.WORKSPACE_FOLDER : target) ?? ConfigurationTarget.WORKSPACE, value);
}
}
@ -465,12 +430,12 @@ export class McpAddConfigurationCommand {
if (packageType) {
this._telemetryService.publicLog2<AddServerCompletedData, AddServerCompletedClassification>('mcp.addserver.completed', {
packageType,
serverType: serverConfig.type,
serverType: config.type,
target: target === ConfigurationTarget.WORKSPACE ? 'workspace' : 'user'
});
}
this.showOnceDiscovered(serverId);
this.showOnceDiscovered(name);
}
public async pickForUrlHandler(resource: URI, showIsPrimary = false): Promise<void> {
@ -478,7 +443,7 @@ export class McpAddConfigurationCommand {
const placeHolder = localize('install.title', 'Install MCP server {0}', name);
const items: IQuickPickItem[] = [
{ id: 'install', label: localize('install.start', 'Install Server'), description: localize('install.description', 'Install in your user settings') },
{ id: 'install', label: localize('install.start', 'Install Server') },
{ id: 'show', label: localize('install.show', 'Show Configuration', name) },
{ id: 'rename', label: localize('install.rename', 'Rename "{0}"', name) },
{ id: 'cancel', label: localize('cancel', 'Cancel') },
@ -498,8 +463,8 @@ export class McpAddConfigurationCommand {
await this._editorService.save(getEditors());
try {
const contents = await this._fileService.readFile(resource);
const { inputs, ...config }: McpConfigurationServer & { inputs?: ConfiguredInput[] } = parseJsonc(contents.value.toString());
await this.writeToUserSetting(name, config, ConfigurationTarget.USER_LOCAL, inputs);
const { inputs, ...config }: IMcpServerConfiguration & { inputs?: IMcpServerVariable[] } = parseJsonc(contents.value.toString());
await this._mcpManagementService.install({ name, config, inputs });
this._editorService.closeEditors(getEditors());
this.showOnceDiscovered(name);
} catch (e) {

View File

@ -20,10 +20,9 @@ import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js';
import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { IMcpConfigPath, IMcpConfigPathsService } from '../common/mcpConfigPathsService.js';
import { mcpConfigurationSection } from '../common/mcpConfiguration.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpService, McpConnectionState } from '../common/mcpTypes.js';
import { IMcpConfigPath, IMcpService, IMcpWorkbenchService, McpConnectionState } from '../common/mcpTypes.js';
const diagnosticOwner = 'vscode.mcp';
@ -33,7 +32,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
constructor(
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
@IMcpConfigPathsService private readonly _mcpConfigPathsService: IMcpConfigPathsService,
@IMcpWorkbenchService private readonly _mcpWorkbenchService: IMcpWorkbenchService,
@IMcpService private readonly _mcpService: IMcpService,
@IMarkerService private readonly _markerService: IMarkerService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
@ -41,8 +40,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
super();
const patterns = [
{ pattern: '**/.vscode/mcp.json' },
{ pattern: '**/settings.json' },
{ pattern: '**/mcp.json' },
{ pattern: '**/workspace.json' },
];
@ -60,13 +58,13 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
}
/** Simple mechanism to avoid extra json parsing for hints+lenses */
private _parseModel(model: ITextModel) {
private async _parseModel(model: ITextModel) {
if (this._cachedMcpSection.value?.model === model) {
return this._cachedMcpSection.value;
}
const uri = model.uri;
const inConfig = this._mcpConfigPathsService.paths.get().find(u => isEqual(u.uri, uri));
const inConfig = await this._mcpWorkbenchService.getMcpConfigPath(model.uri);
if (!inConfig) {
return undefined;
}
@ -140,8 +138,8 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
}
}
private _provideCodeLenses(model: ITextModel, onDidChangeCodeLens: () => void): CodeLensList | undefined {
const parsed = this._parseModel(model);
private async _provideCodeLenses(model: ITextModel, onDidChangeCodeLens: () => void): Promise<CodeLensList | undefined> {
const parsed = await this._parseModel(model);
if (!parsed) {
return undefined;
}
@ -320,7 +318,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
}
private async _provideInlayHints(model: ITextModel, range: Range): Promise<InlayHintList | undefined> {
const parsed = this._parseModel(model);
const parsed = await this._parseModel(model);
if (!parsed) {
return undefined;
}

View File

@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IMcpServerConfiguration, IMcpServerVariable } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IStringDictionary } from '../../../../base/common/collections.js';
import { mcpConfigurationSection } from '../../../contrib/mcp/common/mcpConfiguration.js';
import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { URI } from '../../../../base/common/uri.js';
import { parse } from '../../../../base/common/jsonc.js';
import { isObject } from '../../../../base/common/types.js';
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';
interface IMcpConfiguration {
inputs?: IMcpServerVariable[];
servers?: IStringDictionary<IMcpServerConfiguration>;
}
export class McpConfigMigrationContribution extends Disposable implements IWorkbenchContribution {
static ID = 'workbench.mcp.config.migration';
constructor(
@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@IFileService private readonly fileService: IFileService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
@ILogService private readonly logService: ILogService,
) {
super();
this.migrateMcpConfig();
}
private async migrateMcpConfig(): Promise<void> {
try {
const userMcpConfig = await this.parseMcpConfig(this.userDataProfileService.currentProfile.settingsResource);
if (userMcpConfig && userMcpConfig.servers && Object.keys(userMcpConfig.servers).length > 0) {
await Promise.all(Object.entries(userMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userMcpConfig.inputs : undefined })));
await this.removeMcpConfig(this.userDataProfileService.currentProfile.settingsResource);
}
} catch (error) {
this.logService.error(`MCP migration: Failed to migrate user MCP config`, error);
}
const remoteEnvironment = await this.remoteAgentService.getEnvironment();
if (!remoteEnvironment) {
return;
}
try {
const userRemoteMcpConfig = await this.parseMcpConfig(remoteEnvironment.mcpResource);
if (userRemoteMcpConfig && userRemoteMcpConfig.servers && Object.keys(userRemoteMcpConfig.servers).length > 0) {
await Promise.all(Object.entries(userRemoteMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userRemoteMcpConfig.inputs : undefined }, { target: ConfigurationTarget.USER_REMOTE })));
await this.removeMcpConfig(remoteEnvironment.mcpResource);
}
} catch (error) {
this.logService.error(`MCP migration: Failed to migrate remote MCP config`, error);
}
}
private async parseMcpConfig(settingsFile: URI): Promise<IMcpConfiguration | undefined> {
try {
const content = await this.fileService.readFile(settingsFile);
const settingsObject: IStringDictionary<any> = parse(content.value.toString());
if (!isObject(settingsObject)) {
return undefined;
}
return settingsObject[mcpConfigurationSection] as IMcpConfiguration;
} catch (error) {
this.logService.warn(`MCP migration: Failed to parse MCP config from ${settingsFile}:`, error);
return;
}
}
private async removeMcpConfig(settingsFile: URI): Promise<void> {
try {
await this.jsonEditingService.write(settingsFile, [
{
path: [mcpConfigurationSection],
value: undefined
}
], true);
} catch (error) {
this.logService.warn(`MCP migration: Failed to remove MCP config from ${settingsFile}:`, error);
}
}
}

View File

@ -120,7 +120,7 @@ export class InstallAction extends McpServerAction {
if (!this.mcpServer) {
return;
}
await this.mcpWorkbenchService.install(this.mcpServer);
await this.mcpWorkbenchService.installFromGallery(this.mcpServer);
}
}

View File

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, Dimension, addDisposableListener, append, clearNode, setParentFlowTo } from '../../../../base/browser/dom.js';
import { $, Dimension, append, setParentFlowTo } from '../../../../base/browser/dom.js';
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
@ -36,14 +36,13 @@ import { IWebview, IWebviewService } from '../../webview/browser/webview.js';
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js';
import { InstallCountWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js';
import { IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js';
import { InstallCountWidget, McpServerIconWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js';
import { DropDownAction, InstallAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js';
import { McpServerEditorInput } from './mcpServerEditorInput.js';
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { ILocalMcpServer } from '../../../../platform/mcp/common/mcpManagement.js';
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
const enum McpServerEditorTab {
Readme = 'readme',
@ -114,7 +113,6 @@ interface IActiveElement {
}
interface IExtensionEditorTemplate {
iconContainer: HTMLElement;
name: HTMLElement;
description: HTMLElement;
actionsAndStatusContainer: HTMLElement;
@ -184,6 +182,7 @@ export class McpServerEditor extends EditorPane {
const header = append(root, $('.header'));
const iconContainer = append(header, $('.icon-container'));
const iconWidget = this.instantiationService.createInstance(McpServerIconWidget, iconContainer);
const details = append(header, $('.details'));
const title = append(details, $('.title'));
@ -206,6 +205,7 @@ export class McpServerEditor extends EditorPane {
const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false);
const widgets: McpServerWidget[] = [
iconWidget,
publisherWidget,
installCountWidget,
ratingsWidget,
@ -260,7 +260,6 @@ export class McpServerEditor extends EditorPane {
content,
description,
header,
iconContainer,
name,
navbar,
actionsAndStatusContainer,
@ -297,18 +296,6 @@ export class McpServerEditor extends EditorPane {
this.mcpServerReadme = new Cache(() => mcpServer.getReadme(token));
template.mcpServer = mcpServer;
clearNode(template.iconContainer);
if (mcpServer.iconUrl) {
const icon = append(template.iconContainer, $<HTMLImageElement>('img.icon', { alt: '' }));
this.transientDisposables.add(addDisposableListener(icon, 'error', () => {
clearNode(template.iconContainer);
append(template.iconContainer, $(ThemeIcon.asCSSSelector(mcpServerIcon)));
}, { once: true }));
icon.src = mcpServer.iconUrl;
} else {
append(template.iconContainer, $(ThemeIcon.asCSSSelector(mcpServerIcon)));
}
template.name.textContent = mcpServer.label;
template.name.classList.toggle('clickable', !!mcpServer.url);
template.description.textContent = mcpServer.description;

View File

@ -3,8 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/mcpServersView.css';
import * as dom from '../../../../base/browser/dom.js';
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { IListContextMenuEvent, IListRenderer } from '../../../../base/browser/ui/list/list.js';
import { Event } from '../../../../base/common/event.js';
import { combinedDisposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
@ -19,19 +21,26 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi
import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/views/viewPane.js';
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
import { IViewDescriptorService } from '../../../common/views.js';
import { IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js';
import { IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js';
import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js';
import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js';
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IMcpGalleryService } from '../../../../platform/mcp/common/mcpManagement.js';
import { URI } from '../../../../base/common/uri.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
export class McpServersListView extends ViewPane {
private list: WorkbenchPagedList<IWorkbenchMcpServer> | null = null;
private listContainer: HTMLElement | null = null;
private welcomeContainer: HTMLElement | null = null;
private readonly contextMenuActionRunner = this._register(new ActionRunner());
constructor(
@ -46,6 +55,8 @@ export class McpServersListView extends ViewPane {
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IOpenerService openerService: IOpenerService,
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
@IProductService private readonly productService: IProductService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
}
@ -53,10 +64,14 @@ export class McpServersListView extends ViewPane {
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
const mcpServersList = dom.append(container, dom.$('.mcp-servers-list'));
// Create welcome container
this.welcomeContainer = dom.append(container, dom.$('.mcp-welcome-container.hide'));
this.createWelcomeContent(this.welcomeContainer);
this.listContainer = dom.append(container, dom.$('.mcp-servers-list'));
this.list = this._register(this.instantiationService.createInstance(WorkbenchPagedList,
`${this.id}-MCP-Servers`,
mcpServersList,
this.listContainer,
{
getHeight() { return 72; },
getTemplateId: () => McpServerRenderer.templateId,
@ -127,9 +142,40 @@ export class McpServersListView extends ViewPane {
query = query.trim();
const servers = query ? await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }) : await this.mcpWorkbenchService.queryLocal();
this.list.model = new DelayedPagedModel(new PagedModel(servers));
this.showWelcomeContent(!this.mcpGalleryService.isEnabled() && servers.length === 0);
return this.list.model;
}
private showWelcomeContent(show: boolean): void {
this.welcomeContainer?.classList.toggle('hide', !show);
this.listContainer?.classList.toggle('hide', show);
}
private createWelcomeContent(welcomeContainer: HTMLElement): void {
const welcomeContent = dom.append(welcomeContainer, dom.$('.mcp-welcome-content'));
const iconContainer = dom.append(welcomeContent, dom.$('.mcp-welcome-icon'));
const iconElement = dom.append(iconContainer, dom.$('span'));
iconElement.className = ThemeIcon.asClassName(mcpServerIcon);
const title = dom.append(welcomeContent, dom.$('.mcp-welcome-title'));
title.textContent = localize('mcp.welcome.title', "MCP Servers");
const description = dom.append(welcomeContent, dom.$('.mcp-welcome-description'));
description.textContent = localize('mcp.welcome.description', "Extend agent mode by installing MCP servers to bring extra tools for connecting to databases, invoking APIs, performing specialized tasks, etc.");
// Browse button
const buttonContainer = dom.append(welcomeContent, dom.$('.mcp-welcome-button-container'));
const button = this._register(new Button(buttonContainer, {
title: localize('mcp.welcome.browseButton', "Browse MCP Servers"),
...defaultButtonStyles
}));
button.label = localize('mcp.welcome.browseButton', "Browse MCP Servers");
this._register(button.onDidClick(() => this.openerService.open(URI.parse(this.productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp'))));
}
}
interface IMcpServerTemplateData {

View File

@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { InMemoryFileSystemProvider } from '../../../../platform/files/common/inMemoryFilesystemProvider.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IMcpServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IOpenURLOptions, IURLHandler, IURLService } from '../../../../platform/url/common/url.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
@ -44,7 +44,7 @@ export class McpUrlHandler extends Disposable implements IWorkbenchContribution,
return false;
}
let parsed: McpConfigurationServer & { name: string };
let parsed: IMcpServerConfiguration & { name: string };
try {
parsed = JSON.parse(decodeURIComponent(uri.query));
} catch (e) {

View File

@ -6,24 +6,41 @@
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Emitter } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import { basename } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, InstallMcpServerResult, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { DidUninstallMcpServerEvent, IGalleryMcpServer, IMcpGalleryService, InstallMcpServerResult, IQueryOptions, IMcpServer } from '../../../../platform/mcp/common/mcpManagement.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
import { HasInstalledMcpServersContext, IMcpWorkbenchService, IWorkbenchMcpServer, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { IWorkbenchLocalMcpServer, IWorkbenchMcpManagementService, LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
import { mcpConfigurationSection } from '../common/mcpConfiguration.js';
import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { McpServerEditorInput } from './mcpServerEditorInput.js';
class McpWorkbenchServer implements IWorkbenchMcpServer {
constructor(
public local: ILocalMcpServer | undefined,
public local: IWorkbenchLocalMcpServer | undefined,
public gallery: IGalleryMcpServer | undefined,
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
@IFileService private readonly fileService: IFileService,
) {
this.local = local;
}
get id(): string {
@ -93,13 +110,20 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
constructor(
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
@IMcpManagementService private readonly mcpManagementService: IMcpManagementService,
@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,
@IEditorService private readonly editorService: IEditorService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@ILabelService private readonly labelService: ILabelService,
@IProductService private readonly productService: IProductService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this._register(this.mcpManagementService.onDidInstallMcpServers(e => this.onDidInstallMcpServers(e)));
this._register(this.mcpManagementService.onDidUninstallMcpServer(e => this.onDidUninstallMcpServer(e)));
this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e)));
this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e)));
this.queryLocal().then(async () => {
await this.queryGallery();
this._onChange.fire(undefined);
@ -162,11 +186,15 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
return this._local;
}
async install(server: IWorkbenchMcpServer): Promise<void> {
async install(server: IMcpServer): Promise<void> {
await this.mcpManagementService.install(server);
}
async installFromGallery(server: IWorkbenchMcpServer): Promise<void> {
if (!server.gallery) {
throw new Error('Gallery server is missing');
}
await this.mcpManagementService.installFromGallery(server.gallery, server.gallery.packageTypes[0]);
await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] });
}
async uninstall(server: IWorkbenchMcpServer): Promise<void> {
@ -176,6 +204,104 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
await this.mcpManagementService.uninstall(server.local);
}
getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;
getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;
getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {
if (arg instanceof URI) {
const mcpResource = arg;
for (const profile of this.userDataProfilesService.profiles) {
if (this.uriIdentityService.extUri.isEqual(profile.mcpResource, mcpResource)) {
return this.getUserMcpConfigPath(mcpResource);
}
}
return this.remoteAgentService.getEnvironment().then(remoteEnvironment => {
if (remoteEnvironment && this.uriIdentityService.extUri.isEqual(remoteEnvironment.mcpResource, mcpResource)) {
return this.getRemoteMcpConfigPath(mcpResource);
}
return this.getWorkspaceMcpConfigPath(mcpResource);
});
}
if (arg.scope === LocalMcpServerScope.User) {
return this.getUserMcpConfigPath(arg.mcpResource);
}
if (arg.scope === LocalMcpServerScope.Workspace) {
return this.getWorkspaceMcpConfigPath(arg.mcpResource);
}
if (arg.scope === LocalMcpServerScope.RemoteUser) {
return this.getRemoteMcpConfigPath(arg.mcpResource);
}
return undefined;
}
private getUserMcpConfigPath(mcpResource: URI): IMcpConfigPath {
return {
id: 'usrlocal',
key: 'userLocalValue',
target: ConfigurationTarget.USER_LOCAL,
label: localize('mcp.configuration.userLocalValue', 'Global in {0}', this.productService.nameShort),
scope: StorageScope.PROFILE,
order: McpCollectionSortOrder.User,
uri: mcpResource,
section: [],
};
}
private getRemoteMcpConfigPath(mcpResource: URI): IMcpConfigPath {
return {
id: 'usrremote',
key: 'userRemoteValue',
target: ConfigurationTarget.USER_REMOTE,
label: this.environmentService.remoteAuthority ? this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority) : 'Remote',
scope: StorageScope.PROFILE,
order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,
remoteAuthority: this.environmentService.remoteAuthority,
uri: mcpResource,
section: [],
};
}
private getWorkspaceMcpConfigPath(mcpResource: URI): IMcpConfigPath | undefined {
const workspace = this.workspaceService.getWorkspace();
if (workspace.configuration && this.uriIdentityService.extUri.isEqual(workspace.configuration, mcpResource)) {
return {
id: 'workspace',
key: 'workspaceValue',
target: ConfigurationTarget.WORKSPACE,
label: basename(mcpResource),
scope: StorageScope.WORKSPACE,
order: McpCollectionSortOrder.Workspace,
remoteAuthority: this.environmentService.remoteAuthority,
uri: mcpResource,
section: ['settings', mcpConfigurationSection],
};
}
const workspaceFolders = workspace.folders;
for (let index = 0; index < workspaceFolders.length; index++) {
const workspaceFolder = workspaceFolders[index];
if (this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), mcpResource)) {
return {
id: `wf${index}`,
key: 'workspaceFolderValue',
target: ConfigurationTarget.WORKSPACE_FOLDER,
label: `${workspaceFolder.name}/.vscode/mcp.json`,
scope: StorageScope.WORKSPACE,
remoteAuthority: this.environmentService.remoteAuthority,
order: McpCollectionSortOrder.WorkspaceFolder,
uri: mcpResource,
workspaceFolder,
};
}
}
return undefined;
}
async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise<void> {
await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP);
}

View File

@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.mcp-welcome-container {
height: 100%;
width: 100%;
&.hide {
display: none;
}
.mcp-welcome-content {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
padding: 0px 40px;
text-align: center;
margin: 20px auto;
.mcp-welcome-icon {
.codicon {
font-size: 48px;
}
}
.mcp-welcome-title {
font-size: 24px;
margin-top: 5px;
font-weight: 500;
line-height: normal;
}
.mcp-welcome-description {
max-width: 350px;
padding: 0 20px;
margin-top: 26px;
}
.mcp-welcome-button-container {
margin-top: 24px;
margin-bottom: 24px;
max-width: 300px;
width: 100%;
}
}
}

View File

@ -1,189 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals as arrayEquals } from '../../../../../base/common/arrays.js';
import { Throttler } from '../../../../../base/common/async.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { autorunDelta, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
import { posix as pathPosix, win32 as pathWin32, sep as pathSep } from '../../../../../base/common/path.js';
import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';
import { URI } from '../../../../../base/common/uri.js';
import { Location } from '../../../../../editor/common/languages.js';
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
import { getMcpServerMapping } from '../mcpConfigFileUtils.js';
import { IMcpConfigPath, IMcpConfigPathsService } from '../mcpConfigPathsService.js';
import { IMcpConfiguration, mcpConfigurationSection } from '../mcpConfiguration.js';
import { IMcpRegistry } from '../mcpRegistryTypes.js';
import { McpServerDefinition, McpServerTransportType } from '../mcpTypes.js';
import { IMcpDiscovery } from './mcpDiscovery.js';
interface ConfigSource {
path: IMcpConfigPath;
serverDefinitions: ISettableObservable<readonly McpServerDefinition[]>;
disposable: MutableDisposable<IDisposable>;
getServerToLocationMapping(uri: URI): Promise<Map<string, Location>>;
}
/**
* Discovers MCP servers based on various config sources.
*/
export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery {
private configSources: ConfigSource[] = [];
constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
@ITextModelService private readonly _textModelService: ITextModelService,
@IMcpConfigPathsService private readonly _mcpConfigPathsService: IMcpConfigPathsService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
) {
super();
}
public start() {
const throttler = this._register(new Throttler());
const addPath = (path: IMcpConfigPath) => {
this.configSources.push({
path,
serverDefinitions: observableValue(this, []),
disposable: this._register(new MutableDisposable()),
getServerToLocationMapping: (uri) => this._getServerIdMapping(uri, path.section ? [...path.section, 'servers'] : ['servers']),
});
};
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(mcpConfigurationSection)) {
throttler.queue(() => this.sync());
}
}));
this._register(autorunDelta(this._mcpConfigPathsService.paths, ({ lastValue, newValue }) => {
for (const last of lastValue || []) {
if (!newValue.includes(last)) {
const idx = this.configSources.findIndex(src => src.path.id === last.id);
if (idx !== -1) {
this.configSources[idx].disposable.dispose();
this.configSources.splice(idx, 1);
}
}
}
for (const next of newValue) {
if (!lastValue || !lastValue.includes(next)) {
addPath(next);
}
}
this.sync();
}));
}
private async _getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {
const store = new DisposableStore();
try {
const ref = await this._textModelService.createModelReference(resource);
store.add(ref);
const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });
return serverIdMapping;
} catch {
return new Map();
} finally {
store.dispose();
}
}
private async sync() {
const configurationKey = this._configurationService.inspect<IMcpConfiguration>(mcpConfigurationSection);
const configMappings = await Promise.all(this.configSources.map(src => {
const uri = src.path.uri;
return uri && src.getServerToLocationMapping(uri);
}));
const remoteEnv = await this._remoteAgentService.getEnvironment();
for (const [index, src] of this.configSources.entries()) {
const collectionId = `mcp.config.${src.path.id}`;
// inspect() will give the first workspace folder, and must be
// asked for explicitly for other folders.
let value = src.path.workspaceFolder
? this._configurationService.inspect<IMcpConfiguration>(mcpConfigurationSection, { resource: src.path.workspaceFolder.uri })[src.path.key]
: configurationKey[src.path.key];
// If we see there are MCP servers, migrate them automatically
if (value?.mcpServers) {
value = { ...value, servers: { ...value.servers, ...value.mcpServers }, mcpServers: undefined };
this._configurationService.updateValue(mcpConfigurationSection, value, {}, src.path.target, { donotNotifyError: true });
}
const configMapping = configMappings[index];
const { isAbsolute, join, sep } = src.path.remoteAuthority && remoteEnv
? (remoteEnv.os === OperatingSystem.Windows ? pathWin32 : pathPosix)
: (isWindows ? pathWin32 : pathPosix);
const fsPathForRemote = (uri: URI) => {
const fsPathLocal = uri.fsPath;
return fsPathLocal.replaceAll(pathSep, sep);
};
const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({
id: `${collectionId}.${name}`,
label: name,
launch: 'url' in value ? {
type: McpServerTransportType.HTTP,
uri: URI.parse(value.url),
headers: Object.entries(value.headers || {}),
} : {
type: McpServerTransportType.Stdio,
args: value.args || [],
command: value.command,
env: value.env || {},
envFile: value.envFile,
cwd: value.cwd
// if the cwd is defined in a workspace folder but not absolute (and not
// a variable or tilde-expansion) then resolve it in the workspace folder
? (!isAbsolute(value.cwd) && !value.cwd.startsWith('~') && !value.cwd.startsWith('${') && src.path.workspaceFolder
? join(fsPathForRemote(src.path.workspaceFolder.uri), value.cwd)
: value.cwd)
: src.path.workspaceFolder
? fsPathForRemote(src.path.workspaceFolder.uri)
: undefined,
},
roots: src.path.workspaceFolder ? [src.path.workspaceFolder.uri] : undefined,
variableReplacement: {
folder: src.path.workspaceFolder,
section: mcpConfigurationSection,
target: src.path.target,
},
devMode: value.dev,
presentation: {
order: src.path.order,
origin: configMapping?.get(name),
}
}));
if (arrayEquals(nextDefinitions, src.serverDefinitions.get(), McpServerDefinition.equals)) {
continue;
}
if (!nextDefinitions.length) {
src.disposable.clear();
src.serverDefinitions.set(nextDefinitions, undefined);
} else {
src.serverDefinitions.set(nextDefinitions, undefined);
src.disposable.value ??= this._mcpRegistry.registerCollection({
id: collectionId,
label: src.path.label,
presentation: { order: src.path.order, origin: src.path.uri },
remoteAuthority: src.path.remoteAuthority || null,
serverDefinitions: src.serverDefinitions,
isTrustedByDefault: true,
configTarget: src.path.target,
scope: src.path.scope,
});
}
}
}
}

View File

@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
import { Throttler } from '../../../../../base/common/async.js';
import { URI } from '../../../../../base/common/uri.js';
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
import { StorageScope } from '../../../../../platform/storage/common/storage.js';
import { IMcpRegistry } from '../mcpRegistryTypes.js';
import { McpServerDefinition, McpServerTransportType, IMcpWorkbenchService, IMcpConfigPath } from '../mcpTypes.js';
import { IMcpDiscovery } from './mcpDiscovery.js';
import { mcpConfigurationSection } from '../mcpConfiguration.js';
import { posix as pathPosix, win32 as pathWin32, sep as pathSep } from '../../../../../base/common/path.js';
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { getMcpServerMapping } from '../mcpConfigFileUtils.js';
import { Location } from '../../../../../editor/common/languages.js';
import { ResourceMap } from '../../../../../base/common/map.js';
import { ILocalMcpServer } from '../../../../../platform/mcp/common/mcpManagement.js';
import { observableValue } from '../../../../../base/common/observable.js';
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';
export class InstalledMcpServersDiscovery extends Disposable implements IMcpDiscovery {
private readonly collectionDisposables = this._register(new DisposableMap<string, IDisposable>());
constructor(
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@ITextModelService private readonly textModelService: ITextModelService,
) {
super();
}
public start(): void {
const throttler = this._register(new Throttler());
this._register(this.mcpWorkbenchService.onChange(() => throttler.queue(() => this.sync())));
this.sync();
}
private async getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {
const store = new DisposableStore();
try {
const ref = await this.textModelService.createModelReference(resource);
store.add(ref);
const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });
return serverIdMapping;
} catch {
return new Map();
} finally {
store.dispose();
}
}
private async sync(): Promise<void> {
try {
const remoteEnv = await this.remoteAgentService.getEnvironment();
const collections = new Map<string, [IMcpConfigPath | undefined, McpServerDefinition[]]>();
const mcpConfigPathInfos = new ResourceMap<Promise<IMcpConfigPath & { locations: Map<string, Location> } | undefined>>();
for (const server of this.mcpWorkbenchService.local) {
if (!server.local) {
continue;
}
let mcpConfigPathPromise = mcpConfigPathInfos.get(server.local.mcpResource);
if (!mcpConfigPathPromise) {
mcpConfigPathPromise = (async (local: ILocalMcpServer) => {
const mcpConfigPath = this.mcpWorkbenchService.getMcpConfigPath(local);
const locations = mcpConfigPath?.uri ? await this.getServerIdMapping(mcpConfigPath?.uri, mcpConfigPath.section ? [...mcpConfigPath.section, 'servers'] : ['servers']) : new Map();
return mcpConfigPath ? { ...mcpConfigPath, locations } : undefined;
})(server.local);
mcpConfigPathInfos.set(server.local.mcpResource, mcpConfigPathPromise);
}
const config = server.local.config;
const mcpConfigPath = await mcpConfigPathPromise;
const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;
let definitions = collections.get(collectionId);
if (!definitions) {
definitions = [mcpConfigPath, []];
collections.set(collectionId, definitions);
}
const { isAbsolute, join, sep } = mcpConfigPath?.remoteAuthority && remoteEnv
? (remoteEnv.os === OperatingSystem.Windows ? pathWin32 : pathPosix)
: (isWindows ? pathWin32 : pathPosix);
const fsPathForRemote = (uri: URI) => {
const fsPathLocal = uri.fsPath;
return fsPathLocal.replaceAll(pathSep, sep);
};
definitions[1].push({
id: `${collectionId}.${server.local.name}`,
label: server.local.name,
launch: config.type === 'http' ? {
type: McpServerTransportType.HTTP,
uri: URI.parse(config.url),
headers: Object.entries(config.headers || {}),
} : {
type: McpServerTransportType.Stdio,
command: config.command,
args: config.args || [],
env: config.env || {},
envFile: config.envFile,
cwd: config.cwd
// if the cwd is defined in a workspace folder but not absolute (and not
// a variable or tilde-expansion) then resolve it in the workspace folder
// if the cwd is defined in a workspace folder but not absolute (and not
// a variable or tilde-expansion) then resolve it in the workspace folder
? (!isAbsolute(config.cwd) && !config.cwd.startsWith('~') && !config.cwd.startsWith('${') && mcpConfigPath?.workspaceFolder
? join(fsPathForRemote(mcpConfigPath.workspaceFolder.uri), config.cwd)
: config.cwd)
: mcpConfigPath?.workspaceFolder
? fsPathForRemote(mcpConfigPath.workspaceFolder.uri)
: undefined,
},
roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,
variableReplacement: {
folder: mcpConfigPath?.workspaceFolder,
section: mcpConfigurationSection,
target: mcpConfigPath?.target ?? ConfigurationTarget.USER,
},
devMode: config.dev,
presentation: {
order: mcpConfigPath?.order,
origin: mcpConfigPath?.locations.get(server.local.name)
}
});
}
for (const [id, [mcpConfigPath, serverDefinitions]] of collections) {
this.collectionDisposables.deleteAndDispose(id);
this.collectionDisposables.set(id, this.mcpRegistry.registerCollection({
id: id,
label: mcpConfigPath?.label ?? '',
presentation: {
order: serverDefinitions[0]?.presentation?.order,
origin: mcpConfigPath?.uri,
},
remoteAuthority: mcpConfigPath?.remoteAuthority ?? null,
serverDefinitions: observableValue(this, serverDefinitions),
isTrustedByDefault: true,
configTarget: mcpConfigPath?.target ?? ConfigurationTarget.USER,
scope: mcpConfigPath?.scope ?? StorageScope.PROFILE,
}));
}
for (const [id] of this.collectionDisposables) {
if (!collections.has(id)) {
this.collectionDisposables.deleteAndDispose(id);
}
}
} catch (error) {
this.collectionDisposables.clearAndDisposeAll();
}
}
}

View File

@ -1,152 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
import { basename } from '../../../../base/common/resources.js';
import { isDefined } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { FOLDER_SETTINGS_PATH, IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
import { mcpConfigurationSection } from './mcpConfiguration.js';
import { McpCollectionSortOrder } from './mcpTypes.js';
export interface IMcpConfigPath {
/** Short, unique ID for this config. */
id: string;
/** Configuration scope that maps to this path. */
key: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';
/** Display name */
label: string;
/** Storage where associated data should be stored. */
scope: StorageScope;
/** Configuration target that correspond to this file */
target: ConfigurationTarget;
/** Order in which the configuration should be displayed */
order: number;
/** Config's remote authority */
remoteAuthority?: string;
/** Config file URI. */
uri: URI | undefined;
/** When MCP config is nested in a config file, the parent nested key. */
section?: string[];
/** Workspace folder, when the config refers to a workspace folder value. */
workspaceFolder?: IWorkspaceFolder;
}
export interface IMcpConfigPathsService {
_serviceBrand: undefined;
readonly paths: IObservable<readonly IMcpConfigPath[]>;
}
export const IMcpConfigPathsService = createDecorator<IMcpConfigPathsService>('IMcpConfigPathsService');
export class McpConfigPathsService extends Disposable implements IMcpConfigPathsService {
_serviceBrand: undefined;
private readonly _paths: ISettableObservable<readonly IMcpConfigPath[]>;
public get paths(): IObservable<readonly IMcpConfigPath[]> {
return this._paths;
}
constructor(
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IProductService productService: IProductService,
@ILabelService labelService: ILabelService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IPreferencesService preferencesService: IPreferencesService,
) {
super();
const workspaceConfig = workspaceContextService.getWorkspace().configuration;
const initialPaths: (IMcpConfigPath | undefined | null)[] = [
{
id: 'usrlocal',
key: 'userLocalValue',
target: ConfigurationTarget.USER_LOCAL,
label: localize('mcp.configuration.userLocalValue', 'Global in {0}', productService.nameShort),
scope: StorageScope.PROFILE,
order: McpCollectionSortOrder.User,
uri: preferencesService.userSettingsResource,
section: [mcpConfigurationSection],
},
workspaceConfig && {
id: 'workspace',
key: 'workspaceValue',
target: ConfigurationTarget.WORKSPACE,
label: basename(workspaceConfig),
scope: StorageScope.WORKSPACE,
order: McpCollectionSortOrder.Workspace,
remoteAuthority: _environmentService.remoteAuthority,
uri: workspaceConfig,
section: ['settings', mcpConfigurationSection],
},
...workspaceContextService.getWorkspace()
.folders
.map(wf => this._fromWorkspaceFolder(wf))
];
this._paths = observableValue('mcpConfigPaths', initialPaths.filter(isDefined));
remoteAgentService.getEnvironment().then((env) => {
const label = _environmentService.remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, _environmentService.remoteAuthority) : 'Remote';
this._paths.set([
...this.paths.get(),
{
id: 'usrremote',
key: 'userRemoteValue',
target: ConfigurationTarget.USER_REMOTE,
label,
scope: StorageScope.PROFILE,
order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,
uri: env?.settingsPath,
remoteAuthority: _environmentService.remoteAuthority,
section: [mcpConfigurationSection],
}
], undefined);
});
this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => {
const next = this._paths.get().slice();
for (const folder of e.added) {
next.push(this._fromWorkspaceFolder(folder));
}
for (const folder of e.removed) {
const idx = next.findIndex(c => c.workspaceFolder === folder);
if (idx !== -1) {
next.splice(idx, 1);
}
}
this._paths.set(next, undefined);
}));
}
private _fromWorkspaceFolder(workspaceFolder: IWorkspaceFolder): IMcpConfigPath {
return {
id: `wf${workspaceFolder.index}`,
key: 'workspaceFolderValue',
target: ConfigurationTarget.WORKSPACE_FOLDER,
label: `${workspaceFolder.name}/.vscode/mcp.json`,
scope: StorageScope.WORKSPACE,
remoteAuthority: this._environmentService.remoteAuthority,
order: McpCollectionSortOrder.WorkspaceFolder,
uri: URI.joinPath(workspaceFolder.uri, FOLDER_SETTINGS_PATH, '../mcp.json'),
workspaceFolder,
};
}
}

View File

@ -10,8 +10,6 @@ import { mcpSchemaId } from '../../../services/configuration/common/configuratio
import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js';
import { IExtensionPointDescriptor } from '../../../services/extensions/common/extensionsRegistry.js';
export type { McpConfigurationServer, IMcpConfigurationStdio, IMcpConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
const mcpActivationEventPrefix = 'onMcpCollection:';
/**

View File

@ -20,11 +20,12 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IGalleryMcpServer, ILocalMcpServer, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js';
import { IMcpServer as IInstallableMcpServer, IGalleryMcpServer, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js';
import { IMcpDevModeConfig } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
import { IWorkspaceFolder, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { ToolProgress } from '../../chat/common/languageModelToolsService.js';
import { IMcpServerSamplingConfiguration } from './mcpConfiguration.js';
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
@ -550,6 +551,19 @@ export class MpcResponseError extends Error {
export class McpConnectionFailedError extends Error { }
export interface IMcpConfigPath {
id: string;
key: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';
label: string;
scope: StorageScope;
target: ConfigurationTarget;
order: number;
remoteAuthority?: string;
uri: URI | undefined;
section?: string[];
workspaceFolder?: IWorkspaceFolder;
}
export interface IMcpServerContainer extends IDisposable {
mcpServer: IWorkbenchMcpServer | null;
update(): void;
@ -557,7 +571,7 @@ export interface IMcpServerContainer extends IDisposable {
export interface IWorkbenchMcpServer {
readonly gallery: IGalleryMcpServer | undefined;
readonly local: ILocalMcpServer | undefined;
readonly local: IWorkbenchLocalMcpServer | undefined;
readonly id: string;
readonly name: string;
readonly label: string;
@ -580,8 +594,11 @@ export interface IMcpWorkbenchService {
readonly local: readonly IWorkbenchMcpServer[];
queryLocal(): Promise<IWorkbenchMcpServer[]>;
queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IWorkbenchMcpServer[]>;
install(mcpServer: IWorkbenchMcpServer): Promise<void>;
install(server: IInstallableMcpServer, installOptions?: IWorkbencMcpServerInstallOptions): Promise<void>;
installFromGallery(mcpServer: IWorkbenchMcpServer): Promise<void>;
uninstall(mcpServer: IWorkbenchMcpServer): Promise<void>;
getMcpConfigPath(arg: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;
getMcpConfigPath(arg: URI): Promise<IMcpConfigPath | undefined>;
open(extension: IWorkbenchMcpServer | string, options?: IEditorOptions): Promise<void>;
}

View File

@ -881,6 +881,8 @@ class AbstractProfileResourceTreeRenderer extends Disposable {
return localize('snippets', "Snippets");
case ProfileResourceType.Tasks:
return localize('tasks', "Tasks");
case ProfileResourceType.Mcp:
return localize('mcp', "MCP Servers");
case ProfileResourceType.Extensions:
return localize('extensions', "Extensions");
}

View File

@ -21,6 +21,7 @@ import { SettingsResource, SettingsResourceTreeItem } from '../../../services/us
import { KeybindingsResource, KeybindingsResourceTreeItem } from '../../../services/userDataProfile/browser/keybindingsResource.js';
import { TasksResource, TasksResourceTreeItem } from '../../../services/userDataProfile/browser/tasksResource.js';
import { SnippetsResource, SnippetsResourceTreeItem } from '../../../services/userDataProfile/browser/snippetsResource.js';
import { McpProfileResource, McpResourceTreeItem } from '../../../services/userDataProfile/browser/mcpProfileResource.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { InMemoryFileSystemProvider } from '../../../../platform/files/common/inMemoryFilesystemProvider.js';
@ -250,13 +251,15 @@ export abstract class AbstractUserDataProfileElement extends Disposable {
ProfileResourceType.Settings,
ProfileResourceType.Keybindings,
ProfileResourceType.Tasks,
ProfileResourceType.Mcp,
ProfileResourceType.Snippets,
ProfileResourceType.Extensions
];
return Promise.all(resourceTypes.map<Promise<IProfileResourceTypeElement>>(async r => {
const children = (r === ProfileResourceType.Settings
|| r === ProfileResourceType.Keybindings
|| r === ProfileResourceType.Tasks) ? await this.getChildrenForResourceType(r) : [];
|| r === ProfileResourceType.Tasks
|| r === ProfileResourceType.Mcp) ? await this.getChildrenForResourceType(r) : [];
return {
handle: r,
checkbox: undefined,
@ -295,6 +298,9 @@ export abstract class AbstractUserDataProfileElement extends Disposable {
case ProfileResourceType.Tasks:
children = await this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren();
break;
case ProfileResourceType.Mcp:
children = await this.instantiationService.createInstance(McpResourceTreeItem, profile).getChildren();
break;
case ProfileResourceType.Extensions:
children = await this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren();
break;
@ -656,7 +662,8 @@ export class NewProfileElement extends AbstractUserDataProfileElement {
keybindings: true,
snippets: true,
tasks: true,
extensions: true
extensions: true,
mcp: true
} : undefined;
}
@ -678,6 +685,7 @@ export class NewProfileElement extends AbstractUserDataProfileElement {
this.setCopyFlag(ProfileResourceType.Tasks, !!this.template.tasks);
this.setCopyFlag(ProfileResourceType.Snippets, !!this.template.snippets);
this.setCopyFlag(ProfileResourceType.Extensions, !!this.template.extensions);
this.setCopyFlag(ProfileResourceType.Mcp, !!this.template.mcp);
this._onDidChange.fire({ copyFromInfo: true });
}
return;
@ -695,6 +703,7 @@ export class NewProfileElement extends AbstractUserDataProfileElement {
this.setCopyFlag(ProfileResourceType.Tasks, true);
this.setCopyFlag(ProfileResourceType.Snippets, true);
this.setCopyFlag(ProfileResourceType.Extensions, true);
this.setCopyFlag(ProfileResourceType.Mcp, true);
this._onDidChange.fire({ copyFromInfo: true });
return;
}
@ -710,6 +719,7 @@ export class NewProfileElement extends AbstractUserDataProfileElement {
this.setCopyFlag(ProfileResourceType.Tasks, false);
this.setCopyFlag(ProfileResourceType.Snippets, false);
this.setCopyFlag(ProfileResourceType.Extensions, false);
this.setCopyFlag(ProfileResourceType.Mcp, false);
this._onDidChange.fire({ copyFromInfo: true });
} finally {
this.disabled = false;
@ -831,6 +841,12 @@ export class NewProfileElement extends AbstractUserDataProfileElement {
return this.getChildrenFromProfile(profile, resourceType);
}
return [];
case ProfileResourceType.Mcp:
if (profileTemplate.mcp) {
await this.instantiationService.createInstance(McpProfileResource).apply(profileTemplate.mcp, profile);
return this.getChildrenFromProfile(profile, resourceType);
}
return [];
case ProfileResourceType.Extensions:
if (profileTemplate.extensions) {
const children = await this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren();

View File

@ -603,6 +603,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}, {
id: SyncResource.Tasks,
label: getSyncAreaLabel(SyncResource.Tasks)
}, {
id: SyncResource.Mcp,
label: getSyncAreaLabel(SyncResource.Mcp)
}, {
id: SyncResource.GlobalState,
label: getSyncAreaLabel(SyncResource.GlobalState),

View File

@ -166,6 +166,7 @@ export class UserConfiguration extends Disposable {
constructor(
private settingsResource: URI,
private tasksResource: URI | undefined,
private mcpResource: URI | undefined,
private configurationParseOptions: ConfigurationParseOptions,
private readonly fileService: IFileService,
private readonly uriIdentityService: IUriIdentityService,
@ -177,16 +178,23 @@ export class UserConfiguration extends Disposable {
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.userConfiguration.value!.loadConfiguration().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
}
async reset(settingsResource: URI, tasksResource: URI | undefined, configurationParseOptions: ConfigurationParseOptions): Promise<ConfigurationModel> {
async reset(settingsResource: URI, tasksResource: URI | undefined, mcpResource: URI | undefined, configurationParseOptions: ConfigurationParseOptions): Promise<ConfigurationModel> {
this.settingsResource = settingsResource;
this.tasksResource = tasksResource;
this.mcpResource = mcpResource;
this.configurationParseOptions = configurationParseOptions;
return this.doReset();
}
private async doReset(settingsConfiguration?: ConfigurationModel): Promise<ConfigurationModel> {
const folder = this.uriIdentityService.extUri.dirname(this.settingsResource);
const standAloneConfigurationResources: [string, URI][] = this.tasksResource ? [[TASKS_CONFIGURATION_KEY, this.tasksResource]] : [];
const standAloneConfigurationResources: [string, URI][] = [];
if (this.tasksResource) {
standAloneConfigurationResources.push([TASKS_CONFIGURATION_KEY, this.tasksResource]);
}
if (this.mcpResource) {
standAloneConfigurationResources.push([MCP_CONFIGURATION_KEY, this.mcpResource]);
}
const fileServiceBasedConfiguration = new FileServiceBasedConfiguration(folder.toString(), this.settingsResource, standAloneConfigurationResources, this.configurationParseOptions, this.fileService, this.uriIdentityService, this.logService);
const configurationModel = await fileServiceBasedConfiguration.loadConfiguration(settingsConfiguration);
this.userConfiguration.value = fileServiceBasedConfiguration;

View File

@ -130,7 +130,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat
this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, ConfigurationModel.createEmptyModel(logService), ConfigurationModel.createEmptyModel(logService), ConfigurationModel.createEmptyModel(logService), ConfigurationModel.createEmptyModel(logService), new ResourceMap(), ConfigurationModel.createEmptyModel(logService), new ResourceMap<ConfigurationModel>(), this.workspace, logService);
this.applicationConfigurationDisposables = this._register(new DisposableStore());
this.createApplicationConfiguration();
this.localUserConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, { scopes: getLocalUserConfigurationScopes(userDataProfileService.currentProfile, !!remoteAuthority) }, fileService, uriIdentityService, logService));
this.localUserConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, userDataProfileService.currentProfile.mcpResource, { scopes: getLocalUserConfigurationScopes(userDataProfileService.currentProfile, !!remoteAuthority) }, fileService, uriIdentityService, logService));
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration)));
if (remoteAuthority) {
@ -721,7 +721,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat
private onUserDataProfileChanged(e: DidChangeUserDataProfileEvent): void {
e.join((async () => {
const promises: Promise<ConfigurationModel>[] = [];
promises.push(this.localUserConfiguration.reset(e.profile.settingsResource, e.profile.tasksResource, { scopes: getLocalUserConfigurationScopes(e.profile, !!this.remoteUserConfiguration) }));
promises.push(this.localUserConfiguration.reset(e.profile.settingsResource, e.profile.tasksResource, e.profile.mcpResource, { scopes: getLocalUserConfigurationScopes(e.profile, !!this.remoteUserConfiguration) }));
if (e.previous.isDefault !== e.profile.isDefault
|| !!e.previous.useDefaultFlags?.settings !== !!e.profile.useDefaultFlags?.settings) {
this.createApplicationConfiguration();

View File

@ -0,0 +1,513 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { ILocalMcpServer, IMcpManagementService, IGalleryMcpServer, InstallOptions, InstallMcpServerEvent, UninstallMcpServerEvent, DidUninstallMcpServerEvent, InstallMcpServerResult, IMcpServer, IMcpGalleryService, UninstallOptions } from '../../../../platform/mcp/common/mcpManagement.js';
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platform/mcp/common/mcpResourceScannerService.js';
import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from '../../../../platform/workspace/common/workspace.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../configuration/common/configuration.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
import { URI } from '../../../../base/common/uri.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { McpManagementChannelClient } from '../../../../platform/mcp/common/mcpManagementIpc.js';
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import { IRemoteUserDataProfilesService } from '../../userDataProfile/common/remoteUserDataProfiles.js';
import { AbstractMcpManagementService, ILocalMcpServerInfo } from '../../../../platform/mcp/common/mcpManagementService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { ResourceMap } from '../../../../base/common/map.js';
export interface IWorkbencMcpServerInstallOptions extends InstallOptions {
target?: ConfigurationTarget | IWorkspaceFolder;
}
export const enum LocalMcpServerScope {
User = 'user',
RemoteUser = 'remoteUser',
Workspace = 'workspace',
}
export interface IWorkbenchLocalMcpServer extends ILocalMcpServer {
readonly scope?: LocalMcpServerScope;
}
export interface IWorkbenchMcpServerInstallResult extends InstallMcpServerResult {
readonly local?: IWorkbenchLocalMcpServer;
}
export interface IWorkbenchMcpManagementService extends IMcpManagementService {
readonly onDidInstallMcpServers: Event<readonly IWorkbenchMcpServerInstallResult[]>;
readonly onInstallMcpServerInCurrentProfile: Event<InstallMcpServerEvent>;
readonly onDidInstallMcpServersInCurrentProfile: Event<readonly IWorkbenchMcpServerInstallResult[]>;
readonly onUninstallMcpServerInCurrentProfile: Event<UninstallMcpServerEvent>;
readonly onDidUninstallMcpServerInCurrentProfile: Event<DidUninstallMcpServerEvent>;
getInstalled(): Promise<IWorkbenchLocalMcpServer[]>;
install(server: IMcpServer, options?: IWorkbencMcpServerInstallOptions): Promise<IWorkbenchLocalMcpServer>;
}
export const IWorkbenchMcpManagementService = createDecorator<IWorkbenchMcpManagementService>('workbenchMcpManagementService');
class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpManagementService {
readonly _serviceBrand: undefined;
private _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
readonly onInstallMcpServer = this._onInstallMcpServer.event;
private _onDidInstallMcpServers = this._register(new Emitter<readonly IWorkbenchMcpServerInstallResult[]>());
readonly onDidInstallMcpServers = this._onDidInstallMcpServers.event;
private _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
readonly onUninstallMcpServer = this._onUninstallMcpServer.event;
private _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
readonly onDidUninstallMcpServer = this._onDidUninstallMcpServer.event;
private readonly _onInstallMcpServerInCurrentProfile = this._register(new Emitter<InstallMcpServerEvent>());
readonly onInstallMcpServerInCurrentProfile = this._onInstallMcpServerInCurrentProfile.event;
private readonly _onDidInstallMcpServersInCurrentProfile = this._register(new Emitter<readonly IWorkbenchMcpServerInstallResult[]>());
readonly onDidInstallMcpServersInCurrentProfile = this._onDidInstallMcpServersInCurrentProfile.event;
private readonly _onUninstallMcpServerInCurrentProfile = this._register(new Emitter<UninstallMcpServerEvent>());
readonly onUninstallMcpServerInCurrentProfile = this._onUninstallMcpServerInCurrentProfile.event;
private readonly _onDidUninstallMcpServerInCurrentProfile = this._register(new Emitter<DidUninstallMcpServerEvent>());
readonly onDidUninstallMcpServerInCurrentProfile = this._onDidUninstallMcpServerInCurrentProfile.event;
private readonly workspaceMcpManagementService: IMcpManagementService;
private readonly remoteMcpManagementService: IMcpManagementService | undefined;
constructor(
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IMcpManagementService private readonly mcpManagementService: IMcpManagementService,
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this.workspaceMcpManagementService = this._register(instantiationService.createInstance(WorkspaceMcpManagementService));
const remoteAgentConnection = remoteAgentService.getConnection();
if (remoteAgentConnection) {
this.remoteMcpManagementService = this._register(new McpManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('mcpManagement')));
}
this._register(this.mcpManagementService.onInstallMcpServer(e => {
this._onInstallMcpServer.fire(e);
if (uriIdentityService.extUri.isEqual(e.mcpResource, this.userDataProfileService.currentProfile.mcpResource)) {
this._onInstallMcpServerInCurrentProfile.fire(e);
}
}));
this._register(this.mcpManagementService.onDidInstallMcpServers(e => {
const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = [];
const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = [];
for (const result of e) {
const workbenchResult = {
...result,
local: result.local ? this.toWorkspaceMcpServer(result.local, LocalMcpServerScope.User) : undefined
};
mcpServerInstallResult.push(workbenchResult);
if (uriIdentityService.extUri.isEqual(result.mcpResource, this.userDataProfileService.currentProfile.mcpResource)) {
mcpServerInstallResultInCurrentProfile.push(workbenchResult);
}
}
this._onDidInstallMcpServers.fire(mcpServerInstallResult);
if (mcpServerInstallResultInCurrentProfile.length) {
this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResultInCurrentProfile);
}
}));
this._register(this.mcpManagementService.onUninstallMcpServer(e => {
this._onUninstallMcpServer.fire(e);
if (uriIdentityService.extUri.isEqual(e.mcpResource, this.userDataProfileService.currentProfile.mcpResource)) {
this._onUninstallMcpServerInCurrentProfile.fire(e);
}
}));
this._register(this.mcpManagementService.onDidUninstallMcpServer(e => {
this._onDidUninstallMcpServer.fire(e);
if (uriIdentityService.extUri.isEqual(e.mcpResource, this.userDataProfileService.currentProfile.mcpResource)) {
this._onDidUninstallMcpServerInCurrentProfile.fire(e);
}
}));
this._register(this.workspaceMcpManagementService.onInstallMcpServer(async e => {
this._onInstallMcpServer.fire(e);
this._onInstallMcpServerInCurrentProfile.fire(e);
}));
this._register(this.workspaceMcpManagementService.onDidInstallMcpServers(async e => {
const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = [];
for (const result of e) {
const workbenchResult = {
...result,
local: result.local ? this.toWorkspaceMcpServer(result.local, LocalMcpServerScope.Workspace) : undefined
};
mcpServerInstallResult.push(workbenchResult);
}
this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult);
this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult);
}));
this._register(this.workspaceMcpManagementService.onUninstallMcpServer(async e => {
this._onUninstallMcpServer.fire(e);
this._onUninstallMcpServerInCurrentProfile.fire(e);
}));
this._register(this.workspaceMcpManagementService.onDidUninstallMcpServer(async e => {
this._onDidUninstallMcpServer.fire(e);
this._onDidUninstallMcpServerInCurrentProfile.fire(e);
}));
if (this.remoteMcpManagementService) {
this._register(this.remoteMcpManagementService.onInstallMcpServer(async e => {
this._onInstallMcpServer.fire(e);
const remoteMcpResource = await this.getRemoteMcpResource(this.userDataProfileService.currentProfile.mcpResource);
if (remoteMcpResource ? uriIdentityService.extUri.isEqual(e.mcpResource, remoteMcpResource) : this.userDataProfileService.currentProfile.isDefault) {
this._onInstallMcpServerInCurrentProfile.fire(e);
}
}));
this._register(this.remoteMcpManagementService.onDidInstallMcpServers(async e => {
const mcpServerInstallResult: IWorkbenchMcpServerInstallResult[] = [];
const mcpServerInstallResultInCurrentProfile: IWorkbenchMcpServerInstallResult[] = [];
const remoteMcpResource = await this.getRemoteMcpResource(this.userDataProfileService.currentProfile.mcpResource);
for (const result of e) {
const workbenchResult = {
...result,
local: result.local ? this.toWorkspaceMcpServer(result.local, LocalMcpServerScope.RemoteUser) : undefined
};
mcpServerInstallResult.push(workbenchResult);
if (remoteMcpResource ? uriIdentityService.extUri.isEqual(result.mcpResource, remoteMcpResource) : this.userDataProfileService.currentProfile.isDefault) {
mcpServerInstallResultInCurrentProfile.push(workbenchResult);
}
}
this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResult);
if (mcpServerInstallResultInCurrentProfile.length) {
this._onDidInstallMcpServersInCurrentProfile.fire(mcpServerInstallResultInCurrentProfile);
}
}));
this._register(this.remoteMcpManagementService.onUninstallMcpServer(async e => {
this._onUninstallMcpServer.fire(e);
const remoteMcpResource = await this.getRemoteMcpResource(this.userDataProfileService.currentProfile.mcpResource);
if (remoteMcpResource ? uriIdentityService.extUri.isEqual(e.mcpResource, remoteMcpResource) : this.userDataProfileService.currentProfile.isDefault) {
this._onUninstallMcpServerInCurrentProfile.fire(e);
}
}));
this._register(this.remoteMcpManagementService.onDidUninstallMcpServer(async e => {
this._onDidUninstallMcpServer.fire(e);
const remoteMcpResource = await this.getRemoteMcpResource(this.userDataProfileService.currentProfile.mcpResource);
if (remoteMcpResource ? uriIdentityService.extUri.isEqual(e.mcpResource, remoteMcpResource) : this.userDataProfileService.currentProfile.isDefault) {
this._onDidUninstallMcpServerInCurrentProfile.fire(e);
}
}));
}
}
async getInstalled(): Promise<IWorkbenchLocalMcpServer[]> {
const installed: IWorkbenchLocalMcpServer[] = [];
const [userServers, remoteServers, workspaceServers] = await Promise.all([
this.mcpManagementService.getInstalled(this.userDataProfileService.currentProfile.mcpResource),
this.remoteMcpManagementService?.getInstalled(await this.getRemoteMcpResource()) ?? Promise.resolve<ILocalMcpServer[]>([]),
this.workspaceMcpManagementService?.getInstalled() ?? Promise.resolve<ILocalMcpServer[]>([]),
]);
for (const server of userServers) {
installed.push(this.toWorkspaceMcpServer(server, LocalMcpServerScope.User));
}
for (const server of remoteServers) {
installed.push(this.toWorkspaceMcpServer(server, LocalMcpServerScope.RemoteUser));
}
for (const server of workspaceServers) {
installed.push(this.toWorkspaceMcpServer(server, LocalMcpServerScope.Workspace));
}
return installed;
}
private toWorkspaceMcpServer(server: ILocalMcpServer, scope: LocalMcpServerScope): IWorkbenchLocalMcpServer {
return { ...server, scope };
}
async install(server: IMcpServer, options?: IWorkbencMcpServerInstallOptions): Promise<IWorkbenchLocalMcpServer> {
options = options ?? {};
if (options.target === ConfigurationTarget.WORKSPACE || isWorkspaceFolder(options.target)) {
const mcpResource = options.target === ConfigurationTarget.WORKSPACE ? this.workspaceContextService.getWorkspace().configuration : options.target.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]);
if (!mcpResource) {
throw new Error(`Illegal target: ${options.target}`);
}
options.mcpResource = mcpResource;
return this.workspaceMcpManagementService.install(server, options);
}
if (options.target === ConfigurationTarget.USER_REMOTE) {
if (!this.remoteMcpManagementService) {
throw new Error(`Illegal target: ${options.target}`);
}
options.mcpResource = await this.getRemoteMcpResource(options.mcpResource);
return this.remoteMcpManagementService.install(server, options);
}
if (options.target && options.target !== ConfigurationTarget.USER && options.target !== ConfigurationTarget.USER_LOCAL) {
throw new Error(`Illegal target: ${options.target}`);
}
options.mcpResource = this.userDataProfileService.currentProfile.mcpResource;
return this.mcpManagementService.install(server, options);
}
installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
options = options ?? {};
if (!options.mcpResource) {
options.mcpResource = this.userDataProfileService.currentProfile.mcpResource;
}
return this.mcpManagementService.installFromGallery(server, options);
}
async uninstall(server: IWorkbenchLocalMcpServer): Promise<void> {
if (server.scope === LocalMcpServerScope.Workspace) {
return this.workspaceMcpManagementService.uninstall(server);
}
if (server.scope === LocalMcpServerScope.RemoteUser) {
if (!this.remoteMcpManagementService) {
throw new Error(`Illegal target: ${server.scope}`);
}
return this.remoteMcpManagementService.uninstall(server);
}
return this.mcpManagementService.uninstall(server, { mcpResource: this.userDataProfileService.currentProfile.mcpResource });
}
private async getRemoteMcpResource(mcpResource?: URI): Promise<URI | undefined> {
if (!mcpResource && this.userDataProfileService.currentProfile.isDefault) {
return undefined;
}
mcpResource = mcpResource ?? this.userDataProfileService.currentProfile.mcpResource;
let profile = this.userDataProfilesService.profiles.find(p => this.uriIdentityService.extUri.isEqual(p.mcpResource, mcpResource));
if (profile) {
profile = await this.remoteUserDataProfilesService.getRemoteProfile(profile);
} else {
profile = (await this.remoteUserDataProfilesService.getRemoteProfiles()).find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, mcpResource));
}
return profile?.extensionsResource;
}
}
class WorkspaceMcpResourceManagementService extends AbstractMcpManagementService {
constructor(
target: McpResourceTarget,
private readonly mcpResource: URI,
@IMcpGalleryService mcpGalleryService: IMcpGalleryService,
@IFileService fileService: IFileService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@ILogService logService: ILogService,
@IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService,
) {
super(target, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService);
}
protected getDefaultMcpResource(): URI {
return this.mcpResource;
}
override installFromGallery(): Promise<ILocalMcpServer> {
throw new Error('Not supported');
}
protected override async getLocalMcpServerInfo(): Promise<ILocalMcpServerInfo | undefined> {
return undefined;
}
}
class WorkspaceMcpManagementService extends Disposable implements IMcpManagementService {
readonly _serviceBrand: undefined;
private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
readonly onInstallMcpServer = this._onInstallMcpServer.event;
private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
readonly onDidInstallMcpServers = this._onDidInstallMcpServers.event;
private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
readonly onUninstallMcpServer = this._onUninstallMcpServer.event;
private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
readonly onDidUninstallMcpServer = this._onDidUninstallMcpServer.event;
private allMcpServers: ILocalMcpServer[] = [];
private workspaceConfiguration?: URI | null;
private readonly workspaceMcpManagementServices = new ResourceMap<{ service: IMcpManagementService } & IDisposable>();
constructor(
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@ILogService private readonly logService: ILogService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.initialize();
}
private async initialize(): Promise<void> {
try {
await this.onDidChangeWorkbenchState();
await this.onDidChangeWorkspaceFolders({ added: this.workspaceContextService.getWorkspace().folders, removed: [], changed: [] });
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));
this._register(this.workspaceContextService.onDidChangeWorkbenchState(e => this.onDidChangeWorkbenchState()));
} catch (error) {
this.logService.error('Failed to initialize workspace folders', error);
}
}
private async onDidChangeWorkbenchState(): Promise<void> {
if (this.workspaceConfiguration) {
await this.removeWorkspaceService(this.workspaceConfiguration);
}
this.workspaceConfiguration = this.workspaceContextService.getWorkspace().configuration;
if (this.workspaceConfiguration) {
await this.addWorkspaceService(this.workspaceConfiguration, ConfigurationTarget.WORKSPACE);
}
}
private async onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): Promise<void> {
try {
await Promise.allSettled(e.removed.map(folder => this.removeWorkspaceService(folder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]))));
} catch (error) {
this.logService.error(error);
}
try {
await Promise.allSettled(e.added.map(folder => this.addWorkspaceService(folder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), ConfigurationTarget.WORKSPACE_FOLDER)));
} catch (error) {
this.logService.error(error);
}
}
private async addWorkspaceService(mcpResource: URI, target: McpResourceTarget): Promise<void> {
if (this.workspaceMcpManagementServices.has(mcpResource)) {
return;
}
const disposables = new DisposableStore();
const service = disposables.add(this.instantiationService.createInstance(WorkspaceMcpResourceManagementService, target, mcpResource));
try {
const installedServers = await service.getInstalled();
this.allMcpServers.push(...installedServers);
if (installedServers.length > 0) {
const installResults: InstallMcpServerResult[] = installedServers.map(server => ({
name: server.name,
local: server,
mcpResource: server.mcpResource
}));
this._onDidInstallMcpServers.fire(installResults);
}
} catch (error) {
this.logService.warn('Failed to get installed servers from', mcpResource.toString(), error);
}
disposables.add(service.onInstallMcpServer(e => this._onInstallMcpServer.fire(e)));
disposables.add(service.onDidInstallMcpServers(e => {
for (const { local } of e) {
if (local) {
this.allMcpServers.push(local);
}
}
this._onDidInstallMcpServers.fire(e);
}));
disposables.add(service.onUninstallMcpServer(e => this._onUninstallMcpServer.fire(e)));
disposables.add(service.onDidUninstallMcpServer(e => {
const index = this.allMcpServers.findIndex(server => this.uriIdentityService.extUri.isEqual(server.mcpResource, e.mcpResource) && server.name === e.name);
if (index !== -1) {
this.allMcpServers.splice(index, 1);
this._onDidUninstallMcpServer.fire(e);
}
}));
this.workspaceMcpManagementServices.set(mcpResource, { service, dispose: () => disposables.dispose() });
}
private async removeWorkspaceService(mcpResource: URI): Promise<void> {
const serviceItem = this.workspaceMcpManagementServices.get(mcpResource);
if (serviceItem) {
try {
const installedServers = await serviceItem.service.getInstalled();
this.allMcpServers = this.allMcpServers.filter(server => !installedServers.some(uninstalled => this.uriIdentityService.extUri.isEqual(uninstalled.mcpResource, server.mcpResource)));
for (const server of installedServers) {
this._onDidUninstallMcpServer.fire({
name: server.name,
mcpResource: server.mcpResource
});
}
} catch (error) {
this.logService.warn('Failed to get installed servers from', mcpResource.toString(), error);
}
this.workspaceMcpManagementServices.delete(mcpResource);
serviceItem.dispose();
}
}
async getInstalled(): Promise<ILocalMcpServer[]> {
return this.allMcpServers;
}
async install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
if (!options?.mcpResource) {
throw new Error('MCP resource is required');
}
const mcpManagementServiceItem = this.workspaceMcpManagementServices.get(options?.mcpResource);
if (!mcpManagementServiceItem) {
throw new Error(`No MCP management service found for resource: ${options?.mcpResource.toString()}`);
}
return mcpManagementServiceItem.service.install(server, options);
}
async uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
const mcpResource = server.mcpResource;
const mcpManagementServiceItem = this.workspaceMcpManagementServices.get(mcpResource);
if (!mcpManagementServiceItem) {
throw new Error(`No MCP management service found for resource: ${mcpResource.toString()}`);
}
return mcpManagementServiceItem.service.uninstall(server, options);
}
async installFromGallery(): Promise<ILocalMcpServer> {
throw new Error('Not supported');
}
override dispose(): void {
this.workspaceMcpManagementServices.forEach(service => service.dispose());
this.workspaceMcpManagementServices.clear();
super.dispose();
}
}
registerSingleton(IWorkbenchMcpManagementService, WorkbenchMcpManagementService, InstantiationType.Delayed);

View File

@ -29,6 +29,7 @@ export interface IRemoteAgentEnvironmentDTO {
connectionToken: string;
appRoot: UriComponents;
settingsPath: UriComponents;
mcpResource: UriComponents;
logsPath: UriComponents;
extensionHostLogsPath: UriComponents;
globalStorageHome: UriComponents;
@ -61,6 +62,7 @@ export class RemoteExtensionEnvironmentChannelClient {
connectionToken: data.connectionToken,
appRoot: URI.revive(data.appRoot),
settingsPath: URI.revive(data.settingsPath),
mcpResource: URI.revive(data.mcpResource),
logsPath: URI.revive(data.logsPath),
extensionHostLogsPath: URI.revive(data.extensionHostLogsPath),
globalStorageHome: URI.revive(data.globalStorageHome),

View File

@ -42,6 +42,7 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS
settingsResource: joinPath(inMemoryExtraProfileRoot, 'settingsResource'),
keybindingsResource: joinPath(inMemoryExtraProfileRoot, 'keybindingsResource'),
tasksResource: joinPath(inMemoryExtraProfileRoot, 'tasksResource'),
mcpResource: joinPath(inMemoryExtraProfileRoot, 'mcp.json'),
snippetsHome: joinPath(inMemoryExtraProfileRoot, 'snippetsHome'),
promptsHome: joinPath(inMemoryExtraProfileRoot, 'promptsHome'),
extensionsResource: joinPath(inMemoryExtraProfileRoot, 'extensionsResource'),

View File

@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../base/common/buffer.js';
import { localize } from '../../../../nls.js';
import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IUserDataProfile, ProfileResourceType } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import { API_OPEN_EDITOR_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js';
import { ITreeItemCheckboxState, TreeItemCollapsibleState } from '../../../common/views.js';
import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceInitializer, IProfileResourceTreeItem, IUserDataProfileService } from '../common/userDataProfile.js';
interface IMcpResourceContent {
readonly mcp: string | null;
}
export class McpResourceInitializer implements IProfileResourceInitializer {
constructor(
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
) {
}
async initialize(content: string): Promise<void> {
const mcpContent: IMcpResourceContent = JSON.parse(content);
if (!mcpContent.mcp) {
this.logService.info(`Initializing Profile: No MCP servers to apply...`);
return;
}
await this.fileService.writeFile(this.userDataProfileService.currentProfile.mcpResource, VSBuffer.fromString(mcpContent.mcp));
}
}
export class McpProfileResource implements IProfileResource {
constructor(
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
) {
}
async getContent(profile: IUserDataProfile): Promise<string> {
const mcpContent = await this.getMcpResourceContent(profile);
return JSON.stringify(mcpContent);
}
async getMcpResourceContent(profile: IUserDataProfile): Promise<IMcpResourceContent> {
const mcpContent = await this.getMcpContent(profile);
return { mcp: mcpContent };
}
async apply(content: string, profile: IUserDataProfile): Promise<void> {
const mcpContent: IMcpResourceContent = JSON.parse(content);
if (!mcpContent.mcp) {
this.logService.info(`Importing Profile (${profile.name}): No MCP servers to apply...`);
return;
}
await this.fileService.writeFile(profile.mcpResource, VSBuffer.fromString(mcpContent.mcp));
}
private async getMcpContent(profile: IUserDataProfile): Promise<string | null> {
try {
const content = await this.fileService.readFile(profile.mcpResource);
return content.value.toString();
} catch (error) {
// File not found
if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
return null;
} else {
throw error;
}
}
}
}
export class McpResourceTreeItem implements IProfileResourceTreeItem {
readonly type = ProfileResourceType.Mcp;
readonly handle = ProfileResourceType.Mcp;
readonly label = { label: localize('mcp', "MCP Servers") };
readonly collapsibleState = TreeItemCollapsibleState.Expanded;
checkbox: ITreeItemCheckboxState | undefined;
constructor(
private readonly profile: IUserDataProfile,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) { }
async getChildren(): Promise<IProfileResourceChildTreeItem[]> {
return [{
handle: this.profile.mcpResource.toString(),
resourceUri: this.profile.mcpResource,
collapsibleState: TreeItemCollapsibleState.None,
parent: this,
accessibilityInformation: {
label: this.uriIdentityService.extUri.basename(this.profile.mcpResource)
},
command: {
id: API_OPEN_EDITOR_COMMAND_ID,
title: '',
arguments: [this.profile.mcpResource, undefined, undefined]
}
}];
}
async hasContent(): Promise<boolean> {
const mcpContent = await this.instantiationService.createInstance(McpProfileResource).getMcpResourceContent(this.profile);
return mcpContent.mcp !== null;
}
async getContent(): Promise<string> {
return this.instantiationService.createInstance(McpProfileResource).getContent(this.profile);
}
isFromDefaultProfile(): boolean {
return !this.profile.isDefault && !!this.profile.useDefaultFlags?.mcp;
}
}

View File

@ -748,6 +748,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState {
settingsResource: profile.settingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
keybindingsResource: profile.keybindingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
tasksResource: profile.tasksResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
mcpResource: profile.mcpResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
snippetsHome: profile.snippetsHome.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
promptsHome: profile.promptsHome.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }),
extensionsResource: profile.extensionsResource,

View File

@ -16,6 +16,7 @@ import { GlobalStateResourceInitializer } from './globalStateResource.js';
import { KeybindingsResourceInitializer } from './keybindingsResource.js';
import { TasksResourceInitializer } from './tasksResource.js';
import { SnippetsResourceInitializer } from './snippetsResource.js';
import { McpResourceInitializer } from './mcpProfileResource.js';
import { ExtensionsResourceInitializer } from './extensionsResource.js';
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
import { isString } from '../../../../base/common/types.js';
@ -80,6 +81,9 @@ export class UserDataProfileInitializer implements IUserDataInitializer {
if (profileTemplate?.tasks) {
promises.push(this.initialize(new TasksResourceInitializer(this.userDataProfileService, this.fileService, this.logService), profileTemplate.tasks, ProfileResourceType.Tasks));
}
if (profileTemplate?.mcp) {
promises.push(this.initialize(new McpResourceInitializer(this.userDataProfileService, this.fileService, this.logService), profileTemplate.mcp, ProfileResourceType.Mcp));
}
if (profileTemplate?.snippets) {
promises.push(this.initialize(new SnippetsResourceInitializer(this.userDataProfileService, this.fileService, this.uriIdentityService), profileTemplate.snippets, ProfileResourceType.Snippets));
}

View File

@ -59,6 +59,7 @@ export interface IUserDataProfileTemplate {
readonly snippets?: string;
readonly globalState?: string;
readonly extensions?: string;
readonly mcp?: string;
}
export function isUserDataProfileTemplate(thing: unknown): thing is IUserDataProfileTemplate {
@ -67,7 +68,8 @@ export function isUserDataProfileTemplate(thing: unknown): thing is IUserDataPro
return !!(candidate && typeof candidate === 'object'
&& (isUndefined(candidate.settings) || typeof candidate.settings === 'string')
&& (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string')
&& (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string'));
&& (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')
&& (isUndefined(candidate.mcp) || typeof candidate.mcp === 'string'));
}
export const PROFILE_URL_AUTHORITY = 'profile';

View File

@ -60,6 +60,7 @@ export function getSyncAreaLabel(source: SyncResource): string {
case SyncResource.Snippets: return localize('snippets', "Snippets");
case SyncResource.Prompts: return localize('prompts', "Prompts and Instructions");
case SyncResource.Tasks: return localize('tasks', "Tasks");
case SyncResource.Mcp: return localize('mcp', "MCP Servers");
case SyncResource.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State");
case SyncResource.Profiles: return localize('profiles', "Profiles");

View File

@ -47,6 +47,7 @@ const NULL_PROFILE = {
globalStorageHome: joinPath(homeDir, 'globalStorage'),
keybindingsResource: joinPath(homeDir, 'keybindings.json'),
tasksResource: joinPath(homeDir, 'tasks.json'),
mcpResource: joinPath(homeDir, 'mcp.json'),
snippetsHome: joinPath(homeDir, 'snippets'),
promptsHome: joinPath(homeDir, 'prompts'),
extensionsResource: joinPath(homeDir, 'extensions.json'),

View File

@ -54,6 +54,7 @@ import './browser/parts/statusbar/statusbarPart.js';
import '../platform/actions/common/actions.contribution.js';
import '../platform/undoRedo/common/undoRedoService.js';
import '../platform/mcp/common/mcpResourceScannerService.js';
import './services/workspaces/common/editSessionIdentityService.js';
import './services/workspaces/common/canonicalUriService.js';
import './services/extensions/browser/extensionUrlHandler.js';
@ -82,6 +83,7 @@ import './services/notebook/common/notebookDocumentService.js';
import './services/commands/common/commandService.js';
import './services/themes/browser/workbenchThemeService.js';
import './services/label/common/labelService.js';
import './services/mcp/common/mcpWorkbenchManagementService.js';
import './services/extensions/common/extensionManifestPropertiesService.js';
import './services/extensionManagement/common/extensionGalleryService.js';
import './services/extensionManagement/browser/extensionEnablementService.js';