Skip to content

Commit c8a406e

Browse files
authored
Implement first class resource for user mcp servers (microsoft#252277)
* Revert "Revert mcp.json changes (microsoft#252245)" This reverts commit 94a2502. * fix change event installing mcp servers * properly implement workspace mcp servers
1 parent badd295 commit c8a406e

File tree

57 files changed

+2946
-1020
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2946
-1020
lines changed

src/vs/platform/environment/common/environment.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ export interface INativeEnvironmentService extends IEnvironmentService {
134134
appSettingsHome: URI;
135135
tmpDir: URI;
136136
userDataPath: string;
137-
machineSettingsResource: URI;
138137

139138
// --- extensions
140139
extensionsPath: string;

src/vs/platform/environment/common/environmentService.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
8282
@memoize
8383
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
8484

85-
@memoize
86-
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
87-
8885
@memoize
8986
get workspaceStorageHome(): URI { return joinPath(this.appSettingsHome, 'workspaceStorage'); }
9087

src/vs/platform/mcp/common/mcpManagement.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,31 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationToken } from '../../../base/common/cancellation.js';
7+
import { IStringDictionary } from '../../../base/common/collections.js';
78
import { Event } from '../../../base/common/event.js';
89
import { URI } from '../../../base/common/uri.js';
910
import { SortBy, SortOrder } from '../../extensionManagement/common/extensionManagement.js';
1011
import { createDecorator } from '../../instantiation/common/instantiation.js';
11-
import { IMcpServerConfiguration } from './mcpPlatformTypes.js';
12+
import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js';
13+
14+
export interface IScannedMcpServers {
15+
servers?: IStringDictionary<IScannedMcpServer>;
16+
inputs?: IMcpServerVariable[];
17+
}
18+
19+
export interface IScannedMcpServer {
20+
readonly id: string;
21+
readonly name: string;
22+
readonly version: string;
23+
readonly gallery?: boolean;
24+
readonly config: IMcpServerConfiguration;
25+
}
1226

1327
export interface ILocalMcpServer {
1428
readonly name: string;
1529
readonly config: IMcpServerConfiguration;
1630
readonly version: string;
31+
readonly mcpResource: URI;
1732
readonly location?: URI;
1833
readonly id?: string;
1934
readonly displayName?: string;
@@ -118,43 +133,53 @@ export interface IQueryOptions {
118133
sortOrder?: SortOrder;
119134
}
120135

136+
export const IMcpGalleryService = createDecorator<IMcpGalleryService>('IMcpGalleryService');
137+
export interface IMcpGalleryService {
138+
readonly _serviceBrand: undefined;
139+
isEnabled(): boolean;
140+
query(options?: IQueryOptions, token?: CancellationToken): Promise<IGalleryMcpServer[]>;
141+
getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise<IMcpServerManifest>;
142+
getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise<string>;
143+
}
144+
121145
export interface InstallMcpServerEvent {
122146
readonly name: string;
147+
readonly mcpResource: URI;
123148
readonly source?: IGalleryMcpServer;
124-
readonly applicationScoped?: boolean;
125-
readonly workspaceScoped?: boolean;
126149
}
127150

128151
export interface InstallMcpServerResult {
129152
readonly name: string;
153+
readonly mcpResource: URI;
130154
readonly source?: IGalleryMcpServer;
131155
readonly local?: ILocalMcpServer;
132156
readonly error?: Error;
133-
readonly applicationScoped?: boolean;
134-
readonly workspaceScoped?: boolean;
135157
}
136158

137159
export interface UninstallMcpServerEvent {
138160
readonly name: string;
139-
readonly applicationScoped?: boolean;
140-
readonly workspaceScoped?: boolean;
161+
readonly mcpResource: URI;
141162
}
142163

143164
export interface DidUninstallMcpServerEvent {
144165
readonly name: string;
166+
readonly mcpResource: URI;
145167
readonly error?: string;
146-
readonly applicationScoped?: boolean;
147-
readonly workspaceScoped?: boolean;
148168
}
149169

170+
export type InstallOptions = {
171+
packageType?: PackageType;
172+
mcpResource?: URI;
173+
};
150174

151-
export const IMcpGalleryService = createDecorator<IMcpGalleryService>('IMcpGalleryService');
152-
export interface IMcpGalleryService {
153-
readonly _serviceBrand: undefined;
154-
isEnabled(): boolean;
155-
query(options?: IQueryOptions, token?: CancellationToken): Promise<IGalleryMcpServer[]>;
156-
getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise<IMcpServerManifest>;
157-
getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise<string>;
175+
export type UninstallOptions = {
176+
mcpResource?: URI;
177+
};
178+
179+
export interface IMcpServer {
180+
name: string;
181+
config: IMcpServerConfiguration;
182+
inputs?: IMcpServerVariable[];
158183
}
159184

160185
export const IMcpManagementService = createDecorator<IMcpManagementService>('IMcpManagementService');
@@ -164,9 +189,10 @@ export interface IMcpManagementService {
164189
readonly onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
165190
readonly onUninstallMcpServer: Event<UninstallMcpServerEvent>;
166191
readonly onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
167-
getInstalled(): Promise<ILocalMcpServer[]>;
168-
installFromGallery(server: IGalleryMcpServer, packageType: PackageType): Promise<void>;
169-
uninstall(server: ILocalMcpServer): Promise<void>;
192+
getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;
193+
install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
194+
installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
195+
uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;
170196
}
171197

172198
export const mcpGalleryServiceUrlConfig = 'chat.mcp.gallery.serviceUrl';

src/vs/platform/mcp/common/mcpManagementCli.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,32 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IConfigurationService } from '../../configuration/common/configuration.js';
76
import { ILogger } from '../../log/common/log.js';
8-
import { IMcpConfiguration, IMcpConfigurationHTTP, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js';
7+
import { IMcpServerConfiguration } from './mcpPlatformTypes.js';
8+
import { IMcpManagementService } from './mcpManagement.js';
99

10-
type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationHTTP };
10+
type ValidatedConfig = { name: string; config: IMcpServerConfiguration };
1111

1212
export class McpManagementCli {
1313
constructor(
1414
private readonly _logger: ILogger,
15-
@IConfigurationService private readonly _userConfigurationService: IConfigurationService,
15+
@IMcpManagementService private readonly _mcpManagementService: IMcpManagementService,
1616
) { }
1717

1818
async addMcpDefinitions(
1919
definitions: string[],
2020
) {
2121
const configs = definitions.map((config) => this.validateConfiguration(config));
22-
await this.updateMcpInConfig(this._userConfigurationService, configs);
22+
await this.updateMcpInResource(configs);
2323
this._logger.info(`Added MCP servers: ${configs.map(c => c.name).join(', ')}`);
2424
}
2525

26-
private async updateMcpInConfig(service: IConfigurationService, configs: ValidatedConfig[]) {
27-
const mcp = service.getValue<IMcpConfiguration>('mcp') || { servers: {} };
28-
mcp.servers ??= {};
29-
30-
for (const config of configs) {
31-
mcp.servers[config.name] = config.config;
32-
}
33-
34-
await service.updateValue('mcp', mcp);
26+
private async updateMcpInResource(configs: ValidatedConfig[]) {
27+
await Promise.all(configs.map(({ name, config }) => this._mcpManagementService.install({ name, config })));
3528
}
3629

3730
private validateConfiguration(config: string): ValidatedConfig {
38-
let parsed: McpConfigurationServer & { name: string };
31+
let parsed: IMcpServerConfiguration & { name: string };
3932
try {
4033
parsed = JSON.parse(config);
4134
} catch (e) {
@@ -51,7 +44,7 @@ export class McpManagementCli {
5144
}
5245

5346
const { name, ...rest } = parsed;
54-
return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationHTTP };
47+
return { name, config: rest as IMcpServerConfiguration };
5548
}
5649
}
5750

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Emitter, Event } from '../../../base/common/event.js';
7+
import { Disposable } from '../../../base/common/lifecycle.js';
8+
import { cloneAndChange } from '../../../base/common/objects.js';
9+
import { URI, UriComponents } from '../../../base/common/uri.js';
10+
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from '../../../base/common/uriIpc.js';
11+
import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
12+
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpManagementService, IMcpServer, InstallMcpServerEvent, InstallMcpServerResult, InstallOptions, UninstallMcpServerEvent, UninstallOptions } from './mcpManagement.js';
13+
14+
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI;
15+
function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined;
16+
function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined {
17+
return uri ? URI.revive(transformer ? transformer.transformIncoming(uri) : uri) : undefined;
18+
}
19+
20+
function transformIncomingServer(mcpServer: ILocalMcpServer, transformer: IURITransformer | null): ILocalMcpServer {
21+
transformer = transformer ? transformer : DefaultURITransformer;
22+
const manifest = mcpServer.manifest;
23+
const transformed = transformAndReviveIncomingURIs({ ...mcpServer, ...{ manifest: undefined } }, transformer);
24+
return { ...transformed, ...{ manifest } };
25+
}
26+
27+
function transformIncomingOptions<O extends { mcpResource?: UriComponents }>(options: O | undefined, transformer: IURITransformer | null): O | undefined {
28+
return options?.mcpResource ? transformAndReviveIncomingURIs(options, transformer ?? DefaultURITransformer) : options;
29+
}
30+
31+
function transformOutgoingExtension(extension: ILocalMcpServer, transformer: IURITransformer | null): ILocalMcpServer {
32+
return transformer ? cloneAndChange(extension, value => value instanceof URI ? transformer.transformOutgoingURI(value) : undefined) : extension;
33+
}
34+
35+
function transformOutgoingURI(uri: URI, transformer: IURITransformer | null): URI {
36+
return transformer ? transformer.transformOutgoingURI(uri) : uri;
37+
}
38+
39+
export class McpManagementChannel implements IServerChannel {
40+
readonly onInstallMcpServer: Event<InstallMcpServerEvent>;
41+
readonly onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
42+
readonly onUninstallMcpServer: Event<UninstallMcpServerEvent>;
43+
readonly onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
44+
45+
constructor(private service: IMcpManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) {
46+
this.onInstallMcpServer = Event.buffer(service.onInstallMcpServer, true);
47+
this.onDidInstallMcpServers = Event.buffer(service.onDidInstallMcpServers, true);
48+
this.onUninstallMcpServer = Event.buffer(service.onUninstallMcpServer, true);
49+
this.onDidUninstallMcpServer = Event.buffer(service.onDidUninstallMcpServer, true);
50+
}
51+
52+
listen(context: any, event: string): Event<any> {
53+
const uriTransformer = this.getUriTransformer(context);
54+
switch (event) {
55+
case 'onInstallMcpServer': {
56+
return Event.map<InstallMcpServerEvent, InstallMcpServerEvent>(this.onInstallMcpServer, event => {
57+
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
58+
});
59+
}
60+
case 'onDidInstallMcpServers': {
61+
return Event.map<readonly InstallMcpServerResult[], readonly InstallMcpServerResult[]>(this.onDidInstallMcpServers, results =>
62+
results.map(i => ({
63+
...i,
64+
local: i.local ? transformOutgoingExtension(i.local, uriTransformer) : i.local,
65+
mcpResource: transformOutgoingURI(i.mcpResource, uriTransformer)
66+
})));
67+
}
68+
case 'onUninstallMcpServer': {
69+
return Event.map<UninstallMcpServerEvent, UninstallMcpServerEvent>(this.onUninstallMcpServer, event => {
70+
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
71+
});
72+
}
73+
case 'onDidUninstallMcpServer': {
74+
return Event.map<DidUninstallMcpServerEvent, DidUninstallMcpServerEvent>(this.onDidUninstallMcpServer, event => {
75+
return { ...event, mcpResource: transformOutgoingURI(event.mcpResource, uriTransformer) };
76+
});
77+
}
78+
}
79+
80+
throw new Error('Invalid listen');
81+
}
82+
83+
async call(context: any, command: string, args?: any): Promise<any> {
84+
const uriTransformer: IURITransformer | null = this.getUriTransformer(context);
85+
switch (command) {
86+
case 'getInstalled': {
87+
const mcpServers = await this.service.getInstalled(transformIncomingURI(args[0], uriTransformer));
88+
return mcpServers.map(e => transformOutgoingExtension(e, uriTransformer));
89+
}
90+
case 'install': {
91+
return this.service.install(args[0], transformIncomingOptions(args[1], uriTransformer));
92+
}
93+
case 'installFromGallery': {
94+
return this.service.installFromGallery(args[0], transformIncomingOptions(args[1], uriTransformer));
95+
}
96+
case 'uninstall': {
97+
return this.service.uninstall(transformIncomingServer(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer));
98+
}
99+
}
100+
101+
throw new Error('Invalid call');
102+
}
103+
}
104+
105+
export class McpManagementChannelClient extends Disposable implements IMcpManagementService {
106+
107+
declare readonly _serviceBrand: undefined;
108+
109+
private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
110+
get onInstallMcpServer() { return this._onInstallMcpServer.event; }
111+
112+
private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
113+
get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; }
114+
115+
private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
116+
get onUninstallMcpServer() { return this._onUninstallMcpServer.event; }
117+
118+
private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
119+
get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }
120+
121+
constructor(private readonly channel: IChannel) {
122+
super();
123+
this._register(this.channel.listen<InstallMcpServerEvent>('onInstallMcpServer')(e => this._onInstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
124+
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) })))));
125+
this._register(this.channel.listen<UninstallMcpServerEvent>('onUninstallMcpServer')(e => this._onUninstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
126+
this._register(this.channel.listen<DidUninstallMcpServerEvent>('onDidUninstallMcpServer')(e => this._onDidUninstallMcpServer.fire(({ ...e, mcpResource: transformIncomingURI(e.mcpResource, null) }))));
127+
}
128+
129+
install(server: IMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
130+
return Promise.resolve(this.channel.call<ILocalMcpServer>('install', [server, options])).then(local => transformIncomingServer(local, null));
131+
}
132+
133+
installFromGallery(extension: IGalleryMcpServer, installOptions?: InstallOptions): Promise<ILocalMcpServer> {
134+
return Promise.resolve(this.channel.call<ILocalMcpServer>('installFromGallery', [extension, installOptions])).then(local => transformIncomingServer(local, null));
135+
}
136+
137+
uninstall(extension: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
138+
return Promise.resolve(this.channel.call<void>('uninstall', [extension, options]));
139+
}
140+
141+
getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
142+
return Promise.resolve(this.channel.call<ILocalMcpServer[]>('getInstalled', [mcpResource]))
143+
.then(servers => servers.map(server => transformIncomingServer(server, null)));
144+
}
145+
}

0 commit comments

Comments
 (0)