Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/common/mcpGalleryService.ts
3294 views
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 { CancellationToken } from '../../../base/common/cancellation.js';
7
import { MarkdownString } from '../../../base/common/htmlContent.js';
8
import { Disposable } from '../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../base/common/network.js';
10
import { format2, uppercaseFirstLetter } from '../../../base/common/strings.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { localize } from '../../../nls.js';
13
import { IFileService } from '../../files/common/files.js';
14
import { ILogService } from '../../log/common/log.js';
15
import { IProductService } from '../../product/common/productService.js';
16
import { asJson, asText, IRequestService } from '../../request/common/request.js';
17
import { IGalleryMcpServer, GalleryMcpServerStatus, IMcpGalleryService, IGalleryMcpServerConfiguration, IMcpServerPackage, IMcpServerRemote, IQueryOptions } from './mcpManagement.js';
18
import { IMcpGalleryManifestService, McpGalleryManifestStatus, getMcpGalleryManifestResourceUri, McpGalleryResourceType, IMcpGalleryManifest } from './mcpGalleryManifest.js';
19
import { IPageIterator, IPager, PageIteratorPager, singlePagePager } from '../../../base/common/paging.js';
20
import { CancellationError } from '../../../base/common/errors.js';
21
import { basename } from '../../../base/common/path.js';
22
23
interface IRawGalleryServerListMetadata {
24
readonly count: number;
25
readonly total?: number;
26
readonly next_cursor?: string;
27
}
28
29
interface IGitHubInfo {
30
readonly 'name': string;
31
readonly 'name_with_owner': string;
32
readonly 'is_in_organization'?: boolean;
33
readonly 'license'?: string;
34
readonly 'opengraph_image_url'?: string;
35
readonly 'owner_avatar_url'?: string;
36
readonly 'primary_language'?: string;
37
readonly 'primary_language_color'?: string;
38
readonly 'pushed_at'?: string;
39
readonly 'stargazer_count'?: number;
40
readonly 'topics'?: readonly string[];
41
readonly 'uses_custom_opengraph_image'?: boolean;
42
}
43
44
interface IRawGalleryMcpServerMetaData {
45
readonly 'x-io.modelcontextprotocol.registry'?: {
46
readonly id: string;
47
readonly published_at: string;
48
readonly updated_at: string;
49
readonly is_latest: boolean;
50
readonly release_date?: string;
51
};
52
readonly 'x-publisher'?: Record<string, any>;
53
readonly 'x-github'?: IGitHubInfo;
54
readonly 'github'?: IGitHubInfo;
55
}
56
57
function isIRawGalleryServersOldResult(obj: any): obj is IRawGalleryServersOldResult {
58
return obj && Array.isArray(obj.servers) && isIRawGalleryOldMcpServer(obj.servers[0]);
59
}
60
61
function isIRawGalleryOldMcpServer(obj: any): obj is IRawGalleryOldMcpServer {
62
return obj && obj.server !== undefined;
63
}
64
65
interface IRawGalleryServersResult {
66
readonly metadata?: IRawGalleryServerListMetadata;
67
readonly servers: readonly IRawGalleryMcpServer[];
68
}
69
70
interface IRawGalleryServersOldResult {
71
readonly metadata?: IRawGalleryServerListMetadata;
72
readonly servers: readonly IRawGalleryOldMcpServer[];
73
}
74
75
interface IRawGalleryOldMcpServer extends IRawGalleryMcpServerMetaData {
76
readonly server: IRawGalleryMcpServerDetail;
77
}
78
79
interface IRawGalleryMcpServer extends IRawGalleryMcpServerDetail {
80
readonly _meta?: IRawGalleryMcpServerMetaData;
81
}
82
83
interface IRawGalleryMcpServerPackage extends IMcpServerPackage {
84
readonly registry_name: string;
85
readonly name: string;
86
}
87
88
interface IRawGalleryMcpServerDetail {
89
readonly id: string;
90
readonly name: string;
91
readonly description: string;
92
readonly version_detail: {
93
readonly version: string;
94
readonly release_date: string;
95
readonly is_latest: boolean;
96
};
97
readonly status?: GalleryMcpServerStatus;
98
readonly repository?: {
99
readonly url: string;
100
readonly source: string;
101
readonly id: string;
102
readonly readme?: string;
103
};
104
readonly created_at: string;
105
readonly updated_at: string;
106
readonly packages?: readonly IRawGalleryMcpServerPackage[];
107
readonly remotes?: readonly IMcpServerRemote[];
108
}
109
110
interface IVSCodeGalleryMcpServerDetail {
111
readonly name: string;
112
readonly displayName: string;
113
readonly description: string;
114
readonly repository?: {
115
readonly url: string;
116
readonly source: string;
117
};
118
readonly codicon?: string;
119
readonly iconUrl?: string;
120
readonly iconUrlDark?: string;
121
readonly iconUrlLight?: string;
122
readonly readmeUrl: string;
123
readonly publisher?: {
124
readonly displayName: string;
125
readonly url: string;
126
readonly is_verified: boolean;
127
};
128
readonly manifest: {
129
readonly packages?: readonly IRawGalleryMcpServerPackage[];
130
readonly remotes?: readonly IMcpServerRemote[];
131
};
132
}
133
134
const DefaultPageSize = 50;
135
136
interface IQueryState {
137
readonly searchText?: string;
138
readonly cursor?: string;
139
readonly pageSize: number;
140
}
141
142
const DefaultQueryState: IQueryState = {
143
pageSize: DefaultPageSize,
144
};
145
146
class Query {
147
148
constructor(private state = DefaultQueryState) { }
149
150
get pageSize(): number { return this.state.pageSize; }
151
get searchText(): string | undefined { return this.state.searchText; }
152
153
154
withPage(cursor: string, pageSize: number = this.pageSize): Query {
155
return new Query({ ...this.state, pageSize, cursor });
156
}
157
158
withSearchText(searchText: string): Query {
159
return new Query({ ...this.state, searchText });
160
}
161
}
162
163
export class McpGalleryService extends Disposable implements IMcpGalleryService {
164
165
_serviceBrand: undefined;
166
167
constructor(
168
@IRequestService private readonly requestService: IRequestService,
169
@IFileService private readonly fileService: IFileService,
170
@IProductService private readonly productService: IProductService,
171
@ILogService private readonly logService: ILogService,
172
@IMcpGalleryManifestService private readonly mcpGalleryManifestService: IMcpGalleryManifestService,
173
) {
174
super();
175
}
176
177
isEnabled(): boolean {
178
return this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Available;
179
}
180
181
async query(options?: IQueryOptions, token: CancellationToken = CancellationToken.None): Promise<IPager<IGalleryMcpServer>> {
182
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
183
if (!mcpGalleryManifest) {
184
return singlePagePager([]);
185
}
186
187
const query = new Query();
188
const { servers, metadata } = await this.queryGalleryMcpServers(query, mcpGalleryManifest, token);
189
const total = metadata?.total ?? metadata?.count ?? servers.length;
190
191
const getNextPage = async (cursor: string | undefined, ct: CancellationToken): Promise<IPageIterator<IGalleryMcpServer>> => {
192
if (ct.isCancellationRequested) {
193
throw new CancellationError();
194
}
195
const { servers, metadata } = cursor ? await this.queryGalleryMcpServers(query.withPage(cursor), mcpGalleryManifest, token) : { servers: [], metadata: undefined };
196
return {
197
elements: servers,
198
total,
199
hasNextPage: !!cursor,
200
getNextPage: (token) => getNextPage(metadata?.next_cursor, token)
201
};
202
};
203
204
return new PageIteratorPager({
205
elements: servers,
206
total,
207
hasNextPage: !!metadata?.next_cursor,
208
getNextPage: (token) => getNextPage(metadata?.next_cursor, token),
209
210
});
211
}
212
213
async getMcpServersFromGallery(urls: string[]): Promise<IGalleryMcpServer[]> {
214
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
215
if (!mcpGalleryManifest) {
216
return [];
217
}
218
219
const mcpServers: IGalleryMcpServer[] = [];
220
await Promise.allSettled(urls.map(async url => {
221
const mcpServerUrl = this.getServerUrl(basename(url), mcpGalleryManifest);
222
if (mcpServerUrl !== url) {
223
return;
224
}
225
const mcpServer = await this.getMcpServer(mcpServerUrl);
226
if (mcpServer) {
227
mcpServers.push(mcpServer);
228
}
229
}));
230
231
return mcpServers;
232
}
233
234
async getMcpServersFromVSCodeGallery(names: string[]): Promise<IGalleryMcpServer[]> {
235
const servers = await this.fetchMcpServersFromVSCodeGallery();
236
return servers.filter(item => names.includes(item.name));
237
}
238
239
async getMcpServerConfiguration(gallery: IGalleryMcpServer, token: CancellationToken): Promise<IGalleryMcpServerConfiguration> {
240
if (gallery.configuration) {
241
return gallery.configuration;
242
}
243
244
if (!gallery.url) {
245
throw new Error(`No manifest URL found for ${gallery.name}`);
246
}
247
248
const context = await this.requestService.request({
249
type: 'GET',
250
url: gallery.url,
251
}, token);
252
253
const result = await asJson<IRawGalleryMcpServer | IRawGalleryOldMcpServer>(context);
254
if (!result) {
255
throw new Error(`Failed to fetch configuration from ${gallery.url}`);
256
}
257
258
const server = this.toIRawGalleryMcpServer(result);
259
const configuration = this.toGalleryMcpServerConfiguration(server.packages, server.remotes);
260
if (!configuration) {
261
throw new Error(`Failed to fetch configuration for ${gallery.url}`);
262
}
263
264
return configuration;
265
}
266
267
async getReadme(gallery: IGalleryMcpServer, token: CancellationToken): Promise<string> {
268
const readmeUrl = gallery.readmeUrl;
269
if (!readmeUrl) {
270
return Promise.resolve(localize('noReadme', 'No README available'));
271
}
272
273
const uri = URI.parse(readmeUrl);
274
if (uri.scheme === Schemas.file) {
275
try {
276
const content = await this.fileService.readFile(uri);
277
return content.value.toString();
278
} catch (error) {
279
this.logService.error(`Failed to read file from ${uri}: ${error}`);
280
}
281
}
282
283
if (uri.authority !== 'raw.githubusercontent.com') {
284
return new MarkdownString(localize('readme.viewInBrowser', "You can find information about this server [here]({0})", readmeUrl)).value;
285
}
286
287
const context = await this.requestService.request({
288
type: 'GET',
289
url: readmeUrl,
290
}, token);
291
292
const result = await asText(context);
293
if (!result) {
294
throw new Error(`Failed to fetch README from ${readmeUrl}`);
295
}
296
297
return result;
298
}
299
300
private toGalleryMcpServer(server: IRawGalleryMcpServer, serverUrl: string | undefined): IGalleryMcpServer {
301
const registryInfo = server._meta?.['x-io.modelcontextprotocol.registry'];
302
const githubInfo = server._meta?.['github'] ?? server._meta?.['x-github'];
303
304
let publisher = '';
305
let displayName = '';
306
307
if (githubInfo?.name) {
308
displayName = githubInfo.name.split('-').map(s => uppercaseFirstLetter(s)).join(' ');
309
publisher = githubInfo.name_with_owner.split('/')[0];
310
} else {
311
const nameParts = server.name.split('/');
312
if (nameParts.length > 0) {
313
const domainParts = nameParts[0].split('.');
314
if (domainParts.length > 0) {
315
publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner
316
}
317
}
318
displayName = nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' ');
319
}
320
321
const icon: { light: string; dark: string } | undefined = githubInfo?.owner_avatar_url ? {
322
light: githubInfo.owner_avatar_url,
323
dark: githubInfo.owner_avatar_url
324
} : undefined;
325
326
return {
327
id: server.id,
328
name: server.name,
329
displayName,
330
url: serverUrl,
331
description: server.description,
332
status: server.status ?? GalleryMcpServerStatus.Active,
333
version: server.version_detail.version,
334
isLatest: server.version_detail.is_latest,
335
releaseDate: Date.parse(server.version_detail.release_date),
336
publishDate: registryInfo ? Date.parse(registryInfo.published_at) : undefined,
337
lastUpdated: registryInfo ? Date.parse(registryInfo.updated_at) : undefined,
338
repositoryUrl: server.repository?.url,
339
readme: server.repository?.readme,
340
icon,
341
publisher,
342
license: githubInfo?.license,
343
starsCount: githubInfo?.stargazer_count,
344
topics: githubInfo?.topics,
345
configuration: this.toGalleryMcpServerConfiguration(server.packages, server.remotes)
346
};
347
}
348
349
private toGalleryMcpServerConfiguration(packages?: readonly IRawGalleryMcpServerPackage[], remotes?: readonly IMcpServerRemote[]): IGalleryMcpServerConfiguration | undefined {
350
if (!packages && !remotes) {
351
return undefined;
352
}
353
354
return {
355
packages: packages?.map(p => ({
356
...p,
357
identifier: p.identifier ?? p.name,
358
registry_type: p.registry_type ?? p.registry_name
359
})),
360
remotes
361
};
362
}
363
364
private async queryGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<{ servers: IGalleryMcpServer[]; metadata?: IRawGalleryServerListMetadata }> {
365
if (mcpGalleryManifest.url === this.productService.extensionsGallery?.mcpUrl) {
366
return {
367
servers: await this.fetchMcpServersFromVSCodeGallery()
368
};
369
}
370
const { servers, metadata } = await this.queryRawGalleryMcpServers(query, mcpGalleryManifest, token);
371
return {
372
servers: servers.map(item => this.toGalleryMcpServer(item, this.getServerUrl(item.id, mcpGalleryManifest))),
373
metadata
374
};
375
}
376
377
private async queryRawGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IRawGalleryServersResult> {
378
const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest);
379
if (!mcpGalleryUrl) {
380
return { servers: [] };
381
}
382
383
const uri = URI.parse(mcpGalleryUrl);
384
if (uri.scheme === Schemas.file) {
385
try {
386
const content = await this.fileService.readFile(uri);
387
const data = content.value.toString();
388
return JSON.parse(data);
389
} catch (error) {
390
this.logService.error(`Failed to read file from ${uri}: ${error}`);
391
}
392
}
393
394
const url = `${mcpGalleryUrl}?limit=${query.pageSize}`;
395
396
const context = await this.requestService.request({
397
type: 'GET',
398
url,
399
}, token);
400
401
const result = await asJson<IRawGalleryServersResult | IRawGalleryServersOldResult>(context);
402
403
if (!result) {
404
return { servers: [] };
405
}
406
407
if (isIRawGalleryServersOldResult(result)) {
408
return {
409
servers: result.servers.map<IRawGalleryMcpServer>(server => this.toIRawGalleryMcpServer(server)),
410
metadata: result.metadata
411
};
412
}
413
414
return result;
415
}
416
417
async getMcpServer(mcpServerUrl: string): Promise<IGalleryMcpServer | undefined> {
418
const context = await this.requestService.request({
419
type: 'GET',
420
url: mcpServerUrl,
421
}, CancellationToken.None);
422
423
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
424
return undefined;
425
}
426
427
const server = await asJson<IRawGalleryMcpServer | IRawGalleryOldMcpServer>(context);
428
if (!server) {
429
return undefined;
430
}
431
432
return this.toGalleryMcpServer(this.toIRawGalleryMcpServer(server), mcpServerUrl);
433
}
434
435
private toIRawGalleryMcpServer(from: IRawGalleryOldMcpServer | IRawGalleryMcpServer): IRawGalleryMcpServer {
436
if (isIRawGalleryOldMcpServer(from)) {
437
return {
438
...from.server,
439
_meta: {
440
'x-io.modelcontextprotocol.registry': from['x-io.modelcontextprotocol.registry'],
441
'github': from['x-github'],
442
'x-publisher': from['x-publisher']
443
}
444
};
445
}
446
return from;
447
}
448
449
private async fetchMcpServersFromVSCodeGallery(): Promise<IGalleryMcpServer[]> {
450
const mcpGalleryUrl = this.productService.extensionsGallery?.mcpUrl;
451
if (!mcpGalleryUrl) {
452
return [];
453
}
454
455
const context = await this.requestService.request({
456
type: 'GET',
457
url: mcpGalleryUrl,
458
}, CancellationToken.None);
459
460
const result = await asJson<{ servers: IVSCodeGalleryMcpServerDetail[] }>(context);
461
if (!result) {
462
return [];
463
}
464
465
return result.servers.map<IGalleryMcpServer>(item => {
466
return {
467
id: item.name,
468
name: item.name,
469
displayName: item.displayName,
470
description: item.description,
471
version: '0.0.1',
472
isLatest: true,
473
status: GalleryMcpServerStatus.Active,
474
repositoryUrl: item.repository?.url,
475
codicon: item.codicon,
476
publisher: '',
477
publisherDisplayName: item.publisher?.displayName,
478
publisherDomain: item.publisher ? {
479
link: item.publisher.url,
480
verified: item.publisher.is_verified,
481
} : undefined,
482
readmeUrl: item.readmeUrl,
483
configuration: this.toGalleryMcpServerConfiguration(item.manifest.packages, item.manifest.remotes)
484
};
485
});
486
}
487
488
private getServerUrl(id: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
489
const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerManifestUri);
490
if (!resourceUriTemplate) {
491
return undefined;
492
}
493
return format2(resourceUriTemplate, { id });
494
}
495
496
private getMcpGalleryUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
497
return getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpQueryService);
498
}
499
500
}
501
502