Skip to content

Commit 48aa70f

Browse files
authored
Introduce MCP gallery manifest service (#263238)
1 parent 03baef1 commit 48aa70f

File tree

11 files changed

+301
-37
lines changed

11 files changed

+301
-37
lines changed

src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ import { IMcpResourceScannerService, McpResourceScannerService } from '../../../
131131
import { McpGalleryService } from '../../../platform/mcp/common/mcpGalleryService.js';
132132
import { McpManagementChannel } from '../../../platform/mcp/common/mcpManagementIpc.js';
133133
import { AllowedMcpServersService } from '../../../platform/mcp/common/allowedMcpServersService.js';
134+
import { IMcpGalleryManifestService } from '../../../platform/mcp/common/mcpGalleryManifest.js';
135+
import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGalleryManifestServiceIpc.js';
134136

135137
class SharedProcessMain extends Disposable implements IClientConnectionFilter {
136138

@@ -343,6 +345,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
343345

344346
// MCP Management
345347
services.set(IAllowedMcpServersService, new SyncDescriptor(AllowedMcpServersService, undefined, true));
348+
services.set(IMcpGalleryManifestService, new McpGalleryManifestIPCService(this.server));
346349
services.set(IMcpGalleryService, new SyncDescriptor(McpGalleryService, undefined, true));
347350
services.set(IMcpResourceScannerService, new SyncDescriptor(McpResourceScannerService, undefined, true));
348351
services.set(INpmPackageManagementService, new SyncDescriptor(NpmPackageService, undefined, true));

src/vs/code/node/cliProcessMain.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import { INpmPackageManagementService, NpmPackageService } from '../../platform/
7474
import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js';
7575
import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.js';
7676
import { AllowedMcpServersService } from '../../platform/mcp/common/allowedMcpServersService.js';
77+
import { IMcpGalleryManifestService } from '../../platform/mcp/common/mcpGalleryManifest.js';
78+
import { McpGalleryManifestService } from '../../platform/mcp/common/mcpGalleryManifestService.js';
7779

7880
class CliMain extends Disposable {
7981

@@ -233,6 +235,7 @@ class CliMain extends Disposable {
233235
// MCP
234236
services.set(IAllowedMcpServersService, new SyncDescriptor(AllowedMcpServersService, undefined, true));
235237
services.set(IMcpResourceScannerService, new SyncDescriptor(McpResourceScannerService, undefined, true));
238+
services.set(IMcpGalleryManifestService, new SyncDescriptor(McpGalleryManifestService, undefined, true));
236239
services.set(IMcpGalleryService, new SyncDescriptor(McpGalleryService, undefined, true));
237240
services.set(INpmPackageManagementService, new SyncDescriptor(NpmPackageService, undefined, true));
238241
services.set(IMcpManagementService, new SyncDescriptor(McpManagementService, undefined, true));
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 { Event } from '../../../base/common/event.js';
7+
import { createDecorator } from '../../instantiation/common/instantiation.js';
8+
9+
export const enum McpGalleryResourceType {
10+
McpQueryService = 'McpQueryService',
11+
McpServerManifestUri = 'McpServerManifestUriTemplate',
12+
}
13+
14+
export type McpGalleryManifestResource = {
15+
readonly id: string;
16+
readonly type: string;
17+
};
18+
19+
export interface IMcpGalleryManifest {
20+
readonly resources: readonly McpGalleryManifestResource[];
21+
}
22+
23+
export const enum McpGalleryManifestStatus {
24+
Available = 'available',
25+
Unavailable = 'unavailable'
26+
}
27+
28+
export const IMcpGalleryManifestService = createDecorator<IMcpGalleryManifestService>('IMcpGalleryManifestService');
29+
30+
export interface IMcpGalleryManifestService {
31+
readonly _serviceBrand: undefined;
32+
33+
readonly mcpGalleryManifestStatus: McpGalleryManifestStatus;
34+
readonly onDidChangeMcpGalleryManifestStatus: Event<McpGalleryManifestStatus>;
35+
readonly onDidChangeMcpGalleryManifest: Event<IMcpGalleryManifest | null>;
36+
getMcpGalleryManifest(): Promise<IMcpGalleryManifest | null>;
37+
}
38+
39+
export function getMcpGalleryManifestResourceUri(manifest: IMcpGalleryManifest, type: string): string | undefined {
40+
const [name, version] = type.split('/');
41+
for (const resource of manifest.resources) {
42+
const [r, v] = resource.type.split('/');
43+
if (r !== name) {
44+
continue;
45+
}
46+
if (!version || v === version) {
47+
return resource.id;
48+
}
49+
break;
50+
}
51+
return undefined;
52+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 { Event } from '../../../base/common/event.js';
7+
import { Disposable } from '../../../base/common/lifecycle.js';
8+
import { IProductService } from '../../product/common/productService.js';
9+
import { McpGalleryResourceType, IMcpGalleryManifest, IMcpGalleryManifestService, McpGalleryManifestStatus } from './mcpGalleryManifest.js';
10+
11+
export class McpGalleryManifestService extends Disposable implements IMcpGalleryManifestService {
12+
13+
readonly _serviceBrand: undefined;
14+
readonly onDidChangeMcpGalleryManifest = Event.None;
15+
readonly onDidChangeMcpGalleryManifestStatus = Event.None;
16+
17+
get mcpGalleryManifestStatus(): McpGalleryManifestStatus {
18+
return !!this.productService.extensionsGallery?.mcpUrl ? McpGalleryManifestStatus.Available : McpGalleryManifestStatus.Unavailable;
19+
}
20+
21+
constructor(
22+
@IProductService protected readonly productService: IProductService,
23+
) {
24+
super();
25+
}
26+
27+
async getMcpGalleryManifest(): Promise<IMcpGalleryManifest | null> {
28+
return null;
29+
}
30+
31+
protected createMcpGalleryManifest(mcpUrl: string): IMcpGalleryManifest {
32+
const resources = [
33+
{
34+
id: mcpUrl,
35+
type: McpGalleryResourceType.McpQueryService
36+
},
37+
{
38+
id: `${mcpUrl}/{id}`,
39+
type: McpGalleryResourceType.McpServerManifestUri
40+
}
41+
];
42+
43+
return {
44+
resources
45+
};
46+
}
47+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 { Barrier } from '../../../base/common/async.js';
7+
import { Emitter, Event } from '../../../base/common/event.js';
8+
import { Disposable } from '../../../base/common/lifecycle.js';
9+
import { IPCServer } from '../../../base/parts/ipc/common/ipc.js';
10+
import { IMcpGalleryManifest, IMcpGalleryManifestService, McpGalleryManifestStatus } from './mcpGalleryManifest.js';
11+
12+
export class McpGalleryManifestIPCService extends Disposable implements IMcpGalleryManifestService {
13+
14+
declare readonly _serviceBrand: undefined;
15+
16+
private _onDidChangeMcpGalleryManifest = this._register(new Emitter<IMcpGalleryManifest | null>());
17+
readonly onDidChangeMcpGalleryManifest = this._onDidChangeMcpGalleryManifest.event;
18+
19+
private _onDidChangeMcpGalleryManifestStatus = this._register(new Emitter<McpGalleryManifestStatus>());
20+
readonly onDidChangeMcpGalleryManifestStatus = this._onDidChangeMcpGalleryManifestStatus.event;
21+
22+
private _mcpGalleryManifest: IMcpGalleryManifest | null | undefined;
23+
private readonly barrier = new Barrier();
24+
25+
get mcpGalleryManifestStatus(): McpGalleryManifestStatus {
26+
return this._mcpGalleryManifest ? McpGalleryManifestStatus.Available : McpGalleryManifestStatus.Unavailable;
27+
}
28+
29+
constructor(server: IPCServer<any>) {
30+
super();
31+
server.registerChannel('mcpGalleryManifest', {
32+
listen: () => Event.None,
33+
call: async (context: any, command: string, args?: any): Promise<any> => {
34+
switch (command) {
35+
case 'setMcpGalleryManifest': return Promise.resolve(this.setMcpGalleryManifest(args[0]));
36+
}
37+
throw new Error('Invalid call');
38+
}
39+
});
40+
}
41+
42+
async getMcpGalleryManifest(): Promise<IMcpGalleryManifest | null> {
43+
await this.barrier.wait();
44+
return this._mcpGalleryManifest ?? null;
45+
}
46+
47+
private setMcpGalleryManifest(manifest: IMcpGalleryManifest | null): void {
48+
this._mcpGalleryManifest = manifest;
49+
this._onDidChangeMcpGalleryManifest.fire(manifest);
50+
this._onDidChangeMcpGalleryManifestStatus.fire(this.mcpGalleryManifestStatus);
51+
this.barrier.open();
52+
}
53+
54+
}

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

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import { CancellationToken } from '../../../base/common/cancellation.js';
77
import { MarkdownString } from '../../../base/common/htmlContent.js';
88
import { Disposable } from '../../../base/common/lifecycle.js';
99
import { Schemas } from '../../../base/common/network.js';
10-
import { dirname, joinPath } from '../../../base/common/resources.js';
11-
import { uppercaseFirstLetter } from '../../../base/common/strings.js';
12-
import { isString } from '../../../base/common/types.js';
10+
import { format2, uppercaseFirstLetter } from '../../../base/common/strings.js';
1311
import { URI } from '../../../base/common/uri.js';
1412
import { localize } from '../../../nls.js';
15-
import { IConfigurationService } from '../../configuration/common/configuration.js';
1613
import { IFileService } from '../../files/common/files.js';
1714
import { ILogService } from '../../log/common/log.js';
1815
import { IProductService } from '../../product/common/productService.js';
1916
import { asJson, asText, IRequestService } from '../../request/common/request.js';
20-
import { IGalleryMcpServer, IMcpGalleryService, IMcpServerManifest, IQueryOptions, mcpGalleryServiceUrlConfig, PackageType } from './mcpManagement.js';
17+
import { IGalleryMcpServer, IMcpGalleryService, IMcpServerManifest, IQueryOptions, PackageType } from './mcpManagement.js';
18+
import { IMcpGalleryManifestService, McpGalleryManifestStatus, getMcpGalleryManifestResourceUri, McpGalleryResourceType, IMcpGalleryManifest } from './mcpGalleryManifest.js';
2119

2220
interface IRawGalleryServersResult {
2321
readonly servers: readonly IRawGalleryMcpServer[];
@@ -58,21 +56,26 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
5856
_serviceBrand: undefined;
5957

6058
constructor(
61-
@IConfigurationService private readonly configurationService: IConfigurationService,
6259
@IRequestService private readonly requestService: IRequestService,
6360
@IFileService private readonly fileService: IFileService,
6461
@IProductService private readonly productService: IProductService,
6562
@ILogService private readonly logService: ILogService,
63+
@IMcpGalleryManifestService private readonly mcpGalleryManifestService: IMcpGalleryManifestService,
6664
) {
6765
super();
6866
}
6967

7068
isEnabled(): boolean {
71-
return this.getMcpGalleryUrl() !== undefined;
69+
return this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Available;
7270
}
7371

7472
async query(options?: IQueryOptions, token: CancellationToken = CancellationToken.None): Promise<IGalleryMcpServer[]> {
75-
let { servers } = await this.fetchGallery(token);
73+
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
74+
if (!mcpGalleryManifest) {
75+
return [];
76+
}
77+
78+
let { servers } = await this.fetchGallery(mcpGalleryManifest, token);
7679

7780
if (options?.text) {
7881
const searchText = options.text.toLowerCase();
@@ -81,21 +84,21 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
8184

8285
const galleryServers: IGalleryMcpServer[] = [];
8386
for (const item of servers) {
84-
galleryServers.push(this.toGalleryMcpServer(item));
87+
galleryServers.push(this.toGalleryMcpServer(item, mcpGalleryManifest));
8588
}
8689

8790
return galleryServers;
8891
}
8992

9093
async getMcpServers(names: string[]): Promise<IGalleryMcpServer[]> {
91-
const mcpUrl = this.getMcpGalleryUrl() ?? this.productService.extensionsGallery?.mcpUrl;
92-
if (!mcpUrl) {
94+
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
95+
if (!mcpGalleryManifest) {
9396
return [];
9497
}
9598

96-
const { servers } = await this.fetchGallery(mcpUrl, CancellationToken.None);
99+
const { servers } = await this.fetchGallery(mcpGalleryManifest, CancellationToken.None);
97100
const filteredServers = servers.filter(item => names.includes(item.name));
98-
return filteredServers.map(item => this.toGalleryMcpServer(item));
101+
return filteredServers.map(item => this.toGalleryMcpServer(item, mcpGalleryManifest));
99102
}
100103

101104
async getManifest(gallery: IGalleryMcpServer, token: CancellationToken): Promise<IMcpServerManifest> {
@@ -167,7 +170,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
167170
return result;
168171
}
169172

170-
private toGalleryMcpServer(item: IRawGalleryMcpServer): IGalleryMcpServer {
173+
private toGalleryMcpServer(item: IRawGalleryMcpServer, mcpGalleryManifest: IMcpGalleryManifest): IGalleryMcpServer {
171174
let publisher = '';
172175
const nameParts = item.name.split('/');
173176
if (nameParts.length > 0) {
@@ -178,7 +181,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
178181
}
179182

180183
let icon: { light: string; dark: string } | undefined;
181-
const mcpGalleryUrl = this.getMcpGalleryUrl();
184+
const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest);
182185
if (mcpGalleryUrl && this.productService.extensionsGallery?.mcpUrl !== mcpGalleryUrl) {
183186
if (item.iconUrl) {
184187
icon = {
@@ -206,7 +209,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
206209
codicon: item.codicon,
207210
icon,
208211
readmeUrl: item.readmeUrl,
209-
manifestUrl: this.getManifestUrl(item),
212+
manifestUrl: this.getManifestUrl(item, mcpGalleryManifest),
210213
packageTypes: item.package_types ?? [],
211214
publisher,
212215
publisherDisplayName: item.publisher?.displayName,
@@ -218,15 +221,12 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
218221
};
219222
}
220223

221-
private async fetchGallery(token: CancellationToken): Promise<IRawGalleryServersResult>;
222-
private async fetchGallery(url: string, token: CancellationToken): Promise<IRawGalleryServersResult>;
223-
private async fetchGallery(arg1: any, arg2?: any): Promise<IRawGalleryServersResult> {
224-
const mcpGalleryUrl = isString(arg1) ? arg1 : this.getMcpGalleryUrl();
224+
private async fetchGallery(mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IRawGalleryServersResult> {
225+
const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest);
225226
if (!mcpGalleryUrl) {
226-
return Promise.resolve({ servers: [] });
227+
return { servers: [] };
227228
}
228229

229-
const token = isString(arg1) ? arg2 : arg1;
230230
const uri = URI.parse(mcpGalleryUrl);
231231
if (uri.scheme === Schemas.file) {
232232
try {
@@ -247,26 +247,19 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService
247247
return result || { servers: [] };
248248
}
249249

250-
private getManifestUrl(item: IRawGalleryMcpServer): string | undefined {
251-
const mcpGalleryUrl = this.getMcpGalleryUrl();
252-
253-
if (!mcpGalleryUrl) {
250+
private getManifestUrl(item: IRawGalleryMcpServer, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
251+
if (!item.id) {
254252
return undefined;
255253
}
256-
257-
const uri = URI.parse(mcpGalleryUrl);
258-
if (uri.scheme === Schemas.file) {
259-
return joinPath(dirname(uri), item.id ?? item.name).fsPath;
254+
const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerManifestUri);
255+
if (!resourceUriTemplate) {
256+
return undefined;
260257
}
261-
262-
return `${mcpGalleryUrl}/${item.id}`;
258+
return format2(resourceUriTemplate, { id: item.id });
263259
}
264260

265-
private getMcpGalleryUrl(): string | undefined {
266-
if (this.productService.quality === 'stable') {
267-
return undefined;
268-
}
269-
return this.configurationService.getValue<string>(mcpGalleryServiceUrlConfig);
261+
private getMcpGalleryUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
262+
return getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpQueryService);
270263
}
271264

272265
}

src/vs/server/node/serverServices.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ import { McpGalleryService } from '../../platform/mcp/common/mcpGalleryService.j
9292
import { IMcpResourceScannerService, McpResourceScannerService } from '../../platform/mcp/common/mcpResourceScannerService.js';
9393
import { McpManagementChannel } from '../../platform/mcp/common/mcpManagementIpc.js';
9494
import { AllowedMcpServersService } from '../../platform/mcp/common/allowedMcpServersService.js';
95+
import { IMcpGalleryManifestService } from '../../platform/mcp/common/mcpGalleryManifest.js';
96+
import { McpGalleryManifestIPCService } from '../../platform/mcp/common/mcpGalleryManifestServiceIpc.js';
9597

9698
const eventPrefix = 'monacoworkbench';
9799

@@ -196,6 +198,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
196198
}
197199

198200
services.set(IExtensionGalleryManifestService, new ExtensionGalleryManifestIPCService(socketServer, productService));
201+
services.set(IMcpGalleryManifestService, new McpGalleryManifestIPCService(socketServer));
199202
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
200203

201204
const downloadChannel = socketServer.getChannel('download', router);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js';
7+
import { McpGalleryManifestService as McpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifestService.js';
8+
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
9+
import { IProductService } from '../../../../platform/product/common/productService.js';
10+
import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
11+
12+
class WebMcpGalleryManifestService extends McpGalleryManifestService implements IMcpGalleryManifestService {
13+
14+
constructor(
15+
@IProductService productService: IProductService,
16+
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
17+
) {
18+
super(productService);
19+
const remoteConnection = remoteAgentService.getConnection();
20+
if (remoteConnection) {
21+
const channel = remoteConnection.getChannel('mcpGalleryManifest');
22+
this.getMcpGalleryManifest().then(manifest => {
23+
channel.call('setMcpGalleryManifest', [manifest]);
24+
this._register(this.onDidChangeMcpGalleryManifest(manifest => channel.call('setMcpGalleryManifest', [manifest])));
25+
});
26+
}
27+
}
28+
29+
}
30+
31+
registerSingleton(IMcpGalleryManifestService, WebMcpGalleryManifestService, InstantiationType.Delayed);

0 commit comments

Comments
 (0)