Skip to content

Commit 10a9774

Browse files
authored
1 parent d63338e commit 10a9774

File tree

6 files changed

+136
-40
lines changed

6 files changed

+136
-40
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
8787
return galleryServers;
8888
}
8989

90-
async getMcpServer(name: string): Promise<IGalleryMcpServer | undefined> {
90+
async getMcpServers(names: string[]): Promise<IGalleryMcpServer[]> {
9191
const mcpUrl = this.getMcpGalleryUrl() ?? this.productService.extensionsGallery?.mcpUrl;
9292
if (!mcpUrl) {
93-
return undefined;
93+
return [];
9494
}
9595

9696
const { servers } = await this.fetchGallery(mcpUrl, CancellationToken.None);
97-
const server = servers.find(item => item.name === name);
98-
return server ? this.toGalleryMcpServer(server) : undefined;
97+
const filteredServers = servers.filter(item => names.includes(item.name));
98+
return filteredServers.map(item => this.toGalleryMcpServer(item));
9999
}
100100

101101
async getManifest(gallery: IGalleryMcpServer, token: CancellationToken): Promise<IMcpServerManifest> {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { SortBy, SortOrder } from '../../extensionManagement/common/extensionMan
1010
import { createDecorator } from '../../instantiation/common/instantiation.js';
1111
import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js';
1212

13+
export type InstallSource = 'gallery' | 'local';
14+
1315
export interface ILocalMcpServer {
1416
readonly name: string;
1517
readonly config: IMcpServerConfiguration;
@@ -30,6 +32,7 @@ export interface ILocalMcpServer {
3032
};
3133
readonly codicon?: string;
3234
readonly manifest?: IMcpServerManifest;
35+
readonly source: InstallSource;
3336
}
3437

3538
export interface IMcpServerInput {
@@ -133,7 +136,7 @@ export interface IMcpGalleryService {
133136
readonly _serviceBrand: undefined;
134137
isEnabled(): boolean;
135138
query(options?: IQueryOptions, token?: CancellationToken): Promise<IGalleryMcpServer[]>;
136-
getMcpServer(server: string): Promise<IGalleryMcpServer | undefined>;
139+
getMcpServers(servers: string[]): Promise<IGalleryMcpServer[]>;
137140
getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise<IMcpServerManifest>;
138141
getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise<string>;
139142
}
@@ -189,6 +192,7 @@ export interface IMcpManagementService {
189192
getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;
190193
install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
191194
installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
195+
updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer>;
192196
uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;
193197
}
194198

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ export class McpManagementChannel implements IServerChannel {
106106
case 'uninstall': {
107107
return this.service.uninstall(transformIncomingServer(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer));
108108
}
109+
case 'updateMetadata': {
110+
return this.service.updateMetadata(transformIncomingServer(args[0], uriTransformer), args[1], transformIncomingURI(args[2], uriTransformer));
111+
}
109112
}
110113

111114
throw new Error('Invalid call');
@@ -156,4 +159,8 @@ export class McpManagementChannelClient extends Disposable implements IMcpManage
156159
return Promise.resolve(this.channel.call<ILocalMcpServer[]>('getInstalled', [mcpResource]))
157160
.then(servers => servers.map(server => transformIncomingServer(server, null)));
158161
}
162+
163+
updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise<ILocalMcpServer> {
164+
return Promise.resolve(this.channel.call<ILocalMcpServer>('updateMetadata', [local, gallery, mcpResource])).then(local => transformIncomingServer(local, null));
165+
}
159166
}

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

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ILocalMcpServerInfo {
4040
manifest?: IMcpServerManifest;
4141
readmeUrl?: URI;
4242
location?: URI;
43+
licenseUrl?: string;
4344
}
4445

4546
export abstract class AbstractMcpResourceManagementService extends Disposable implements IMcpManagementService {
@@ -184,7 +185,8 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im
184185
readmeUrl: mcpServerInfo.readmeUrl,
185186
icon: mcpServerInfo.icon,
186187
codicon: mcpServerInfo.codicon,
187-
manifest: mcpServerInfo.manifest
188+
manifest: mcpServerInfo.manifest,
189+
source: config.gallery ? 'gallery' : 'local'
188190
};
189191
}
190192

@@ -382,6 +384,7 @@ export abstract class AbstractMcpResourceManagementService extends Disposable im
382384
}
383385

384386
abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
387+
abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation: URI): Promise<ILocalMcpServer>;
385388
protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined>;
386389
}
387390

@@ -408,30 +411,8 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem
408411
this._onInstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });
409412

410413
try {
411-
const manifest = await this.mcpGalleryService.getManifest(server, CancellationToken.None);
412-
const location = this.getLocation(server.name, server.version);
413-
const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
414-
await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify({
415-
id: server.id,
416-
name: server.name,
417-
displayName: server.displayName,
418-
description: server.description,
419-
version: server.version,
420-
publisher: server.publisher,
421-
publisherDisplayName: server.publisherDisplayName,
422-
repository: server.repositoryUrl,
423-
licenseUrl: server.licenseUrl,
424-
icon: server.icon,
425-
codicon: server.codicon,
426-
...manifest,
427-
})));
428-
429-
if (server.readmeUrl) {
430-
const readme = await this.mcpGalleryService.getReadme(server, CancellationToken.None);
431-
await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
432-
}
414+
const manifest = await this.updateMetadataFromGallery(server);
433415
const { config, inputs } = this.toScannedMcpServerAndInputs(manifest, options?.packageType);
434-
435416
const installable: IInstallableMcpServer = {
436417
name: server.name,
437418
config: {
@@ -456,6 +437,44 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem
456437
}
457438
}
458439

440+
async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer): Promise<ILocalMcpServer> {
441+
await this.updateMetadataFromGallery(gallery);
442+
await this.updateLocal();
443+
const updatedLocal = (await this.getInstalled()).find(s => s.name === local.name);
444+
if (!updatedLocal) {
445+
throw new Error(`Failed to find MCP server: ${local.name}`);
446+
}
447+
return updatedLocal;
448+
}
449+
450+
private async updateMetadataFromGallery(gallery: IGalleryMcpServer): Promise<IMcpServerManifest> {
451+
const manifest = await this.mcpGalleryService.getManifest(gallery, CancellationToken.None);
452+
const location = this.getLocation(gallery.name, gallery.version);
453+
const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
454+
const local: ILocalMcpServerInfo = {
455+
id: gallery.id,
456+
name: gallery.name,
457+
displayName: gallery.displayName,
458+
description: gallery.description,
459+
version: gallery.version,
460+
publisher: gallery.publisher,
461+
publisherDisplayName: gallery.publisherDisplayName,
462+
repositoryUrl: gallery.repositoryUrl,
463+
licenseUrl: gallery.licenseUrl,
464+
icon: gallery.icon,
465+
codicon: gallery.codicon,
466+
manifest,
467+
};
468+
await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify(local)));
469+
470+
if (gallery.readmeUrl) {
471+
const readme = await this.mcpGalleryService.getReadme(gallery, CancellationToken.None);
472+
await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
473+
}
474+
475+
return manifest;
476+
}
477+
459478
protected async getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined> {
460479
let storedMcpServerInfo: ILocalMcpServerInfo | undefined;
461480
let location: URI | undefined;
@@ -549,6 +568,10 @@ export class McpManagementService extends Disposable implements IMcpManagementSe
549568
return this.getMcpResourceManagementService(mcpResourceUri).installFromGallery(server, options);
550569
}
551570

571+
async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise<ILocalMcpServer> {
572+
return this.getMcpResourceManagementService(mcpResource || this.userDataProfilesService.defaultProfile.mcpResource).updateMetadata(local, gallery);
573+
}
574+
552575
override dispose(): void {
553576
this.mcpResourceManagementServices.forEach(service => service.dispose());
554577
this.mcpResourceManagementServices.clear();

src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
1717
import { IFileService } from '../../../../platform/files/common/files.js';
1818
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1919
import { ILabelService } from '../../../../platform/label/common/label.js';
20-
import { DidUninstallMcpServerEvent, IGalleryMcpServer, IMcpGalleryService, InstallMcpServerResult, IQueryOptions, IInstallableMcpServer, IMcpServerManifest } from '../../../../platform/mcp/common/mcpManagement.js';
20+
import { DidUninstallMcpServerEvent, IGalleryMcpServer, IMcpGalleryService, InstallMcpServerResult, IQueryOptions, IInstallableMcpServer, IMcpServerManifest, ILocalMcpServer } from '../../../../platform/mcp/common/mcpManagement.js';
2121
import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
2222
import { IProductService } from '../../../../platform/product/common/productService.js';
2323
import { StorageScope } from '../../../../platform/storage/common/storage.js';
@@ -157,10 +157,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
157157
this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e)));
158158
this._register(this.mcpManagementService.onDidUpdateMcpServersInCurrentProfile(e => this.onDidUpdateMcpServers(e)));
159159
this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e)));
160-
this.queryLocal().then(async () => {
161-
await this.queryGallery();
162-
this._onChange.fire(undefined);
163-
});
160+
this.queryLocal().then(() => this.syncInstalledMcpServers());
164161
urlService.registerHandler(this);
165162
}
166163

@@ -177,20 +174,24 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
177174
}
178175

179176
private onDidInstallMcpServers(e: readonly InstallMcpServerResult[]) {
177+
const servers: IWorkbenchMcpServer[] = [];
180178
for (const result of e) {
181179
if (!result.local) {
182180
continue;
183181
}
184-
this.onDidInstallMcpServer(result.local);
182+
servers.push(this.onDidInstallMcpServer(result.local, result.source));
183+
}
184+
if (servers.some(server => server.local?.source === 'gallery' && !server.gallery)) {
185+
this.syncInstalledMcpServers();
185186
}
186187
}
187188

188-
private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer): IWorkbenchMcpServer {
189+
private onDidInstallMcpServer(local: IWorkbenchLocalMcpServer, gallery?: IGalleryMcpServer): IWorkbenchMcpServer {
189190
let server = this._local.find(server => server.local?.name === local.name);
190191
if (server) {
191192
server.local = local;
192193
} else {
193-
server = this.instantiationService.createInstance(McpWorkbenchServer, local, undefined, undefined);
194+
server = this.instantiationService.createInstance(McpWorkbenchServer, local, gallery, undefined);
194195
this._local.push(server);
195196
}
196197
this._onChange.fire(server);
@@ -225,6 +226,45 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
225226
return undefined;
226227
}
227228

229+
private async syncInstalledMcpServers(): Promise<void> {
230+
const installedGalleryServers: ILocalMcpServer[] = [];
231+
for (const installed of this.local) {
232+
if (installed.local?.source !== 'gallery') {
233+
continue;
234+
}
235+
installedGalleryServers.push(installed.local);
236+
}
237+
if (installedGalleryServers.length) {
238+
const galleryServers = await this.mcpGalleryService.getMcpServers(installedGalleryServers.map(server => server.name));
239+
if (galleryServers.length) {
240+
this.syncInstalledMcpServersWithGallery(galleryServers);
241+
}
242+
}
243+
}
244+
245+
private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise<void> {
246+
const galleryMap = new Map<string, IGalleryMcpServer>(gallery.map(server => [server.name, server]));
247+
for (const mcpServer of this.local) {
248+
if (!mcpServer.gallery) {
249+
if (!mcpServer.local) {
250+
continue;
251+
}
252+
if (mcpServer.gallery) {
253+
continue;
254+
}
255+
const galleryServer = galleryMap.get(mcpServer.name);
256+
if (!galleryServer) {
257+
continue;
258+
}
259+
mcpServer.gallery = galleryServer;
260+
if (!mcpServer.id) {
261+
mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer);
262+
}
263+
this._onChange.fire(mcpServer);
264+
}
265+
}
266+
}
267+
228268
async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IWorkbenchMcpServer[]> {
229269
if (!this.mcpGalleryService.isEnabled()) {
230270
return [];
@@ -378,7 +418,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
378418
const { name, inputs, gallery, ...config } = parsed;
379419

380420
if (gallery || !config || Object.keys(config).length === 0) {
381-
const galleryServer = await this.mcpGalleryService.getMcpServer(name);
421+
const [galleryServer] = await this.mcpGalleryService.getMcpServers([name]);
382422
if (!galleryServer) {
383423
throw new Error(`MCP server '${name}' not found in gallery`);
384424
}

src/vs/workbench/services/mcp/common/mcpWorkbenchManagementService.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,21 @@ class WorkbenchMcpManagementService extends Disposable implements IWorkbenchMcpM
298298
return this.mcpManagementService.installFromGallery(server, options);
299299
}
300300

301+
updateMetadata(local: IWorkbenchLocalMcpServer, server: IGalleryMcpServer, profileLocation: URI): Promise<ILocalMcpServer> {
302+
if (local.scope === LocalMcpServerScope.Workspace) {
303+
return this.workspaceMcpManagementService.updateMetadata(local, server, profileLocation);
304+
}
305+
306+
if (local.scope === LocalMcpServerScope.RemoteUser) {
307+
if (!this.remoteMcpManagementService) {
308+
throw new Error(`Illegal target: ${local.scope}`);
309+
}
310+
return this.remoteMcpManagementService.updateMetadata(local, server, profileLocation);
311+
}
312+
313+
return this.mcpManagementService.updateMetadata(local, server, profileLocation);
314+
}
315+
301316
async uninstall(server: IWorkbenchLocalMcpServer): Promise<void> {
302317
if (server.scope === LocalMcpServerScope.Workspace) {
303318
return this.workspaceMcpManagementService.uninstall(server);
@@ -346,10 +361,13 @@ class WorkspaceMcpResourceManagementService extends AbstractMcpResourceManagemen
346361
throw new Error('Not supported');
347362
}
348363

364+
override updateMetadata(): Promise<ILocalMcpServer> {
365+
throw new Error('Not supported');
366+
}
367+
349368
protected override async getLocalServerInfo(): Promise<ILocalMcpServerInfo | undefined> {
350369
return undefined;
351370
}
352-
353371
}
354372

355373
class WorkspaceMcpManagementService extends Disposable implements IMcpManagementService {
@@ -522,7 +540,11 @@ class WorkspaceMcpManagementService extends Disposable implements IMcpManagement
522540
return mcpManagementServiceItem.service.uninstall(server, options);
523541
}
524542

525-
async installFromGallery(): Promise<ILocalMcpServer> {
543+
installFromGallery(): Promise<ILocalMcpServer> {
544+
throw new Error('Not supported');
545+
}
546+
547+
updateMetadata(): Promise<ILocalMcpServer> {
526548
throw new Error('Not supported');
527549
}
528550

0 commit comments

Comments
 (0)