Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/common/mcpGalleryService.ts
5263 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 { asJson, asText, IRequestService } from '../../request/common/request.js';
16
import { GalleryMcpServerStatus, IGalleryMcpServer, IMcpGalleryService, IMcpServerArgument, IMcpServerInput, IMcpServerKeyValueInput, IMcpServerPackage, IQueryOptions, RegistryType, SseTransport, StreamableHttpTransport, Transport, TransportType } from './mcpManagement.js';
17
import { IMcpGalleryManifestService, McpGalleryManifestStatus, getMcpGalleryManifestResourceUri, McpGalleryResourceType, IMcpGalleryManifest } from './mcpGalleryManifest.js';
18
import { IIterativePager, IIterativePage } from '../../../base/common/paging.js';
19
import { CancellationError } from '../../../base/common/errors.js';
20
import { isObject, isString } from '../../../base/common/types.js';
21
22
interface IMcpRegistryInfo {
23
readonly isLatest?: boolean;
24
readonly publishedAt?: string;
25
readonly updatedAt?: string;
26
}
27
28
interface IGitHubInfo {
29
readonly name: string;
30
readonly nameWithOwner: string;
31
readonly displayName?: string;
32
readonly isInOrganization?: boolean;
33
readonly license?: string;
34
readonly opengraphImageUrl?: string;
35
readonly ownerAvatarUrl?: string;
36
readonly preferredImage?: string;
37
readonly primaryLanguage?: string;
38
readonly primaryLanguageColor?: string;
39
readonly pushedAt?: string;
40
readonly readme?: string;
41
readonly stargazerCount?: number;
42
readonly topics?: readonly string[];
43
readonly usesCustomOpengraphImage?: boolean;
44
}
45
46
interface IAzureAPICenterInfo {
47
readonly 'x-ms-icon'?: string;
48
}
49
50
interface IRawGalleryMcpServersMetadata {
51
readonly count: number;
52
readonly nextCursor?: string;
53
}
54
55
interface IRawGalleryMcpServersResult {
56
readonly metadata: IRawGalleryMcpServersMetadata;
57
readonly servers: readonly IRawGalleryMcpServer[];
58
}
59
60
interface IGalleryMcpServersResult {
61
readonly metadata: IRawGalleryMcpServersMetadata;
62
readonly servers: IGalleryMcpServer[];
63
}
64
65
interface IRawGalleryMcpServer {
66
readonly name: string;
67
readonly description: string;
68
readonly version: string;
69
readonly id?: string;
70
readonly title?: string;
71
readonly repository?: {
72
readonly source: string;
73
readonly url: string;
74
readonly id?: string;
75
};
76
readonly readme?: string;
77
readonly icons?: readonly IRawGalleryMcpServerIcon[];
78
readonly status?: GalleryMcpServerStatus;
79
readonly websiteUrl?: string;
80
readonly createdAt?: string;
81
readonly updatedAt?: string;
82
readonly packages?: readonly IMcpServerPackage[];
83
readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;
84
readonly registryInfo?: IMcpRegistryInfo;
85
readonly githubInfo?: IGitHubInfo;
86
readonly apicInfo?: IAzureAPICenterInfo;
87
}
88
89
interface IGalleryMcpServerDataSerializer {
90
toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined;
91
toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined;
92
}
93
94
interface IRawGalleryMcpServerIcon {
95
readonly src: string;
96
readonly theme?: IconTheme;
97
readonly sizes?: string[];
98
readonly mimeType?: IconMimeType;
99
}
100
101
const enum IconMimeType {
102
PNG = 'image/png',
103
JPEG = 'image/jpeg',
104
JPG = 'image/jpg',
105
SVG = 'image/svg+xml',
106
WEBP = 'image/webp',
107
}
108
109
const enum IconTheme {
110
LIGHT = 'light',
111
DARK = 'dark',
112
}
113
114
namespace McpServerSchemaVersion_v2025_07_09 {
115
116
export const VERSION = 'v0-2025-07-09';
117
export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json`;
118
119
interface RawGalleryMcpServerInput {
120
readonly description?: string;
121
readonly is_required?: boolean;
122
readonly format?: 'string' | 'number' | 'boolean' | 'filepath';
123
readonly value?: string;
124
readonly is_secret?: boolean;
125
readonly default?: string;
126
readonly choices?: readonly string[];
127
}
128
129
interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {
130
readonly variables?: Record<string, RawGalleryMcpServerInput>;
131
}
132
133
interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {
134
readonly type: 'positional';
135
readonly value_hint?: string;
136
readonly is_repeated?: boolean;
137
}
138
139
interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {
140
readonly type: 'named';
141
readonly name: string;
142
readonly is_repeated?: boolean;
143
}
144
145
interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {
146
readonly name: string;
147
readonly value?: string;
148
}
149
150
type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;
151
152
interface McpServerDeprecatedRemote {
153
readonly transport_type?: 'streamable' | 'sse';
154
readonly transport?: 'streamable' | 'sse';
155
readonly url: string;
156
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
157
}
158
159
type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport | McpServerDeprecatedRemote>;
160
161
type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;
162
163
interface StdioTransport {
164
readonly type: 'stdio';
165
}
166
167
interface StreamableHttpTransport {
168
readonly type: 'streamable-http' | 'sse';
169
readonly url: string;
170
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
171
}
172
173
interface SseTransport {
174
readonly type: 'sse';
175
readonly url: string;
176
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
177
}
178
179
interface RawGalleryMcpServerPackage {
180
readonly registry_name: string;
181
readonly name: string;
182
readonly registry_type: 'npm' | 'pypi' | 'docker-hub' | 'nuget' | 'remote' | 'mcpb';
183
readonly registry_base_url?: string;
184
readonly identifier: string;
185
readonly version: string;
186
readonly file_sha256?: string;
187
readonly transport?: RawGalleryTransport;
188
readonly package_arguments?: readonly RawGalleryMcpServerArgument[];
189
readonly runtime_hint?: string;
190
readonly runtime_arguments?: readonly RawGalleryMcpServerArgument[];
191
readonly environment_variables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
192
}
193
194
interface RawGalleryMcpServer {
195
readonly $schema: string;
196
readonly name: string;
197
readonly description: string;
198
readonly status?: 'active' | 'deprecated';
199
readonly repository?: {
200
readonly source: string;
201
readonly url: string;
202
readonly id?: string;
203
readonly readme?: string;
204
};
205
readonly version: string;
206
readonly website_url?: string;
207
readonly created_at: string;
208
readonly updated_at: string;
209
readonly packages?: readonly RawGalleryMcpServerPackage[];
210
readonly remotes?: RawGalleryMcpServerRemotes;
211
readonly _meta: {
212
readonly 'io.modelcontextprotocol.registry/official': {
213
readonly id: string;
214
readonly is_latest: boolean;
215
readonly published_at: string;
216
readonly updated_at: string;
217
readonly release_date?: string;
218
};
219
readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record<string, unknown>;
220
};
221
}
222
223
interface RawGalleryMcpServersResult {
224
readonly metadata: {
225
readonly count: number;
226
readonly next_cursor?: string;
227
};
228
readonly servers: readonly RawGalleryMcpServer[];
229
}
230
231
interface RawGitHubInfo {
232
readonly name: string;
233
readonly name_with_owner: string;
234
readonly display_name?: string;
235
readonly is_in_organization?: boolean;
236
readonly license?: string;
237
readonly opengraph_image_url?: string;
238
readonly owner_avatar_url?: string;
239
readonly primary_language?: string;
240
readonly primary_language_color?: string;
241
readonly pushed_at?: string;
242
readonly stargazer_count?: number;
243
readonly topics?: readonly string[];
244
readonly uses_custom_opengraph_image?: boolean;
245
}
246
247
class Serializer implements IGalleryMcpServerDataSerializer {
248
249
public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {
250
if (!input || typeof input !== 'object' || !Array.isArray((input as RawGalleryMcpServersResult).servers)) {
251
return undefined;
252
}
253
254
const from = <RawGalleryMcpServersResult>input;
255
256
const servers: IRawGalleryMcpServer[] = [];
257
for (const server of from.servers) {
258
const rawServer = this.toRawGalleryMcpServer(server);
259
if (!rawServer) {
260
return undefined;
261
}
262
servers.push(rawServer);
263
}
264
265
return {
266
metadata: {
267
count: from.metadata.count ?? 0,
268
nextCursor: from.metadata?.next_cursor
269
},
270
servers
271
};
272
}
273
274
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
275
if (!input || typeof input !== 'object') {
276
return undefined;
277
}
278
279
const from = <RawGalleryMcpServer>input;
280
281
if (
282
(!from.name || !isString(from.name))
283
|| (!from.description || !isString(from.description))
284
|| (!from.version || !isString(from.version))
285
) {
286
return undefined;
287
}
288
289
if (from.$schema && from.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {
290
return undefined;
291
}
292
293
const registryInfo = from._meta?.['io.modelcontextprotocol.registry/official'];
294
295
function convertServerInput(input: RawGalleryMcpServerInput): IMcpServerInput {
296
return {
297
...input,
298
isRequired: input.is_required,
299
isSecret: input.is_secret,
300
};
301
}
302
303
function convertVariables(variables: Record<string, RawGalleryMcpServerInput>): Record<string, IMcpServerInput> {
304
const result: Record<string, IMcpServerInput> = {};
305
for (const [key, value] of Object.entries(variables)) {
306
result[key] = convertServerInput(value);
307
}
308
return result;
309
}
310
311
function convertServerArgument(arg: RawGalleryMcpServerArgument): IMcpServerArgument {
312
if (arg.type === 'positional') {
313
return {
314
...arg,
315
valueHint: arg.value_hint,
316
isRepeated: arg.is_repeated,
317
isRequired: arg.is_required,
318
isSecret: arg.is_secret,
319
variables: arg.variables ? convertVariables(arg.variables) : undefined,
320
};
321
}
322
return {
323
...arg,
324
isRepeated: arg.is_repeated,
325
isRequired: arg.is_required,
326
isSecret: arg.is_secret,
327
variables: arg.variables ? convertVariables(arg.variables) : undefined,
328
};
329
}
330
331
function convertKeyValueInput(input: RawGalleryMcpServerKeyValueInput): IMcpServerKeyValueInput {
332
return {
333
...input,
334
isRequired: input.is_required,
335
isSecret: input.is_secret,
336
variables: input.variables ? convertVariables(input.variables) : undefined,
337
};
338
}
339
340
function convertTransport(input: RawGalleryTransport): Transport {
341
switch (input.type) {
342
case 'stdio':
343
return {
344
type: TransportType.STDIO,
345
};
346
case 'streamable-http':
347
return {
348
type: TransportType.STREAMABLE_HTTP,
349
url: input.url,
350
headers: input.headers?.map(convertKeyValueInput),
351
};
352
case 'sse':
353
return {
354
type: TransportType.SSE,
355
url: input.url,
356
headers: input.headers?.map(convertKeyValueInput),
357
};
358
default:
359
return {
360
type: TransportType.STDIO,
361
};
362
}
363
}
364
365
function convertRegistryType(input: string): RegistryType {
366
switch (input) {
367
case 'npm':
368
return RegistryType.NODE;
369
case 'docker':
370
case 'docker-hub':
371
case 'oci':
372
return RegistryType.DOCKER;
373
case 'pypi':
374
return RegistryType.PYTHON;
375
case 'nuget':
376
return RegistryType.NUGET;
377
case 'mcpb':
378
return RegistryType.MCPB;
379
default:
380
return RegistryType.NODE;
381
}
382
}
383
384
const gitHubInfo: RawGitHubInfo | undefined = from._meta['io.modelcontextprotocol.registry/publisher-provided']?.github as RawGitHubInfo | undefined;
385
386
return {
387
id: registryInfo.id,
388
name: from.name,
389
description: from.description,
390
repository: from.repository ? {
391
url: from.repository.url,
392
source: from.repository.source,
393
id: from.repository.id,
394
} : undefined,
395
readme: from.repository?.readme,
396
version: from.version,
397
createdAt: from.created_at,
398
updatedAt: from.updated_at,
399
packages: from.packages?.map<IMcpServerPackage>(p => ({
400
identifier: p.identifier ?? p.name,
401
registryType: convertRegistryType(p.registry_type ?? p.registry_name),
402
version: p.version,
403
fileSha256: p.file_sha256,
404
registryBaseUrl: p.registry_base_url,
405
transport: p.transport ? convertTransport(p.transport) : { type: TransportType.STDIO },
406
packageArguments: p.package_arguments?.map(convertServerArgument),
407
runtimeHint: p.runtime_hint,
408
runtimeArguments: p.runtime_arguments?.map(convertServerArgument),
409
environmentVariables: p.environment_variables?.map(convertKeyValueInput),
410
})),
411
remotes: from.remotes?.map(remote => {
412
const type = (<RawGalleryTransport>remote).type ?? (<McpServerDeprecatedRemote>remote).transport_type ?? (<McpServerDeprecatedRemote>remote).transport;
413
return {
414
type: type === TransportType.SSE ? TransportType.SSE : TransportType.STREAMABLE_HTTP,
415
url: remote.url,
416
headers: remote.headers?.map(convertKeyValueInput)
417
};
418
}),
419
registryInfo: {
420
isLatest: registryInfo.is_latest,
421
publishedAt: registryInfo.published_at,
422
updatedAt: registryInfo.updated_at,
423
},
424
githubInfo: gitHubInfo ? {
425
name: gitHubInfo.name,
426
nameWithOwner: gitHubInfo.name_with_owner,
427
displayName: gitHubInfo.display_name,
428
isInOrganization: gitHubInfo.is_in_organization,
429
license: gitHubInfo.license,
430
opengraphImageUrl: gitHubInfo.opengraph_image_url,
431
ownerAvatarUrl: gitHubInfo.owner_avatar_url,
432
primaryLanguage: gitHubInfo.primary_language,
433
primaryLanguageColor: gitHubInfo.primary_language_color,
434
pushedAt: gitHubInfo.pushed_at,
435
stargazerCount: gitHubInfo.stargazer_count,
436
topics: gitHubInfo.topics,
437
usesCustomOpengraphImage: gitHubInfo.uses_custom_opengraph_image
438
} : undefined
439
};
440
}
441
}
442
443
export const SERIALIZER = new Serializer();
444
}
445
446
namespace McpServerSchemaVersion_v0_1 {
447
448
export const VERSION = 'v0.1';
449
450
interface RawGalleryMcpServerInput {
451
readonly choices?: readonly string[];
452
readonly default?: string;
453
readonly description?: string;
454
readonly format?: 'string' | 'number' | 'boolean' | 'filepath';
455
readonly isRequired?: boolean;
456
readonly isSecret?: boolean;
457
readonly placeholder?: string;
458
readonly value?: string;
459
}
460
461
interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {
462
readonly variables?: Record<string, RawGalleryMcpServerInput>;
463
}
464
465
interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {
466
readonly type: 'positional';
467
readonly valueHint?: string;
468
readonly isRepeated?: boolean;
469
}
470
471
interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {
472
readonly type: 'named';
473
readonly name: string;
474
readonly isRepeated?: boolean;
475
}
476
477
interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {
478
readonly name: string;
479
}
480
481
type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;
482
483
type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport>;
484
485
type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;
486
487
interface StdioTransport {
488
readonly type: TransportType.STDIO;
489
}
490
491
interface StreamableHttpTransport {
492
readonly type: TransportType.STREAMABLE_HTTP;
493
readonly url: string;
494
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
495
}
496
497
interface SseTransport {
498
readonly type: TransportType.SSE;
499
readonly url: string;
500
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
501
}
502
503
interface RawGalleryMcpServerPackage {
504
readonly identifier: string;
505
readonly registryType: RegistryType;
506
readonly transport: RawGalleryTransport;
507
readonly fileSha256?: string;
508
readonly environmentVariables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
509
readonly packageArguments?: readonly RawGalleryMcpServerArgument[];
510
readonly registryBaseUrl?: string;
511
readonly runtimeArguments?: readonly RawGalleryMcpServerArgument[];
512
readonly runtimeHint?: string;
513
readonly version?: string;
514
}
515
516
interface RawGalleryMcpServer {
517
readonly name: string;
518
readonly description: string;
519
readonly version: string;
520
readonly $schema: string;
521
readonly title?: string;
522
readonly icons?: IRawGalleryMcpServerIcon[];
523
readonly repository?: {
524
readonly source: string;
525
readonly url: string;
526
readonly subfolder?: string;
527
readonly id?: string;
528
};
529
readonly websiteUrl?: string;
530
readonly packages?: readonly RawGalleryMcpServerPackage[];
531
readonly remotes?: RawGalleryMcpServerRemotes;
532
readonly _meta?: {
533
readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record<string, unknown>;
534
} & IAzureAPICenterInfo;
535
}
536
537
interface RawGalleryMcpServerInfo {
538
readonly server: RawGalleryMcpServer;
539
readonly _meta: {
540
readonly 'io.modelcontextprotocol.registry/official'?: {
541
readonly status: GalleryMcpServerStatus;
542
readonly isLatest: boolean;
543
readonly publishedAt: string;
544
readonly updatedAt?: string;
545
};
546
};
547
}
548
549
interface RawGalleryMcpServersResult {
550
readonly metadata: {
551
readonly count: number;
552
readonly nextCursor?: string;
553
};
554
readonly servers: readonly RawGalleryMcpServerInfo[];
555
}
556
557
class Serializer implements IGalleryMcpServerDataSerializer {
558
559
public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {
560
if (!input || typeof input !== 'object' || !Array.isArray((input as RawGalleryMcpServersResult).servers)) {
561
return undefined;
562
}
563
564
const from = <RawGalleryMcpServersResult>input;
565
566
const servers: IRawGalleryMcpServer[] = [];
567
for (const server of from.servers) {
568
const rawServer = this.toRawGalleryMcpServer(server);
569
if (!rawServer) {
570
if (servers.length === 0) {
571
return undefined;
572
} else {
573
continue;
574
}
575
}
576
servers.push(rawServer);
577
}
578
579
return {
580
metadata: from.metadata,
581
servers
582
};
583
}
584
585
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
586
if (!input || typeof input !== 'object') {
587
return undefined;
588
}
589
590
const from = <RawGalleryMcpServerInfo>input;
591
592
if (
593
(!from.server || !isObject(from.server))
594
|| (!from.server.name || !isString(from.server.name))
595
|| (!from.server.description || !isString(from.server.description))
596
|| (!from.server.version || !isString(from.server.version))
597
) {
598
return undefined;
599
}
600
601
const { 'io.modelcontextprotocol.registry/official': registryInfo, ...apicInfo } = from._meta;
602
const githubInfo = from.server._meta?.['io.modelcontextprotocol.registry/publisher-provided']?.github as IGitHubInfo | undefined;
603
604
return {
605
name: from.server.name,
606
description: from.server.description,
607
version: from.server.version,
608
title: from.server.title,
609
repository: from.server.repository ? {
610
url: from.server.repository.url,
611
source: from.server.repository.source,
612
id: from.server.repository.id,
613
} : undefined,
614
readme: githubInfo?.readme,
615
icons: from.server.icons,
616
websiteUrl: from.server.websiteUrl,
617
packages: from.server.packages,
618
remotes: from.server.remotes,
619
status: registryInfo?.status,
620
registryInfo,
621
githubInfo,
622
apicInfo
623
};
624
}
625
}
626
627
export const SERIALIZER = new Serializer();
628
}
629
630
namespace McpServerSchemaVersion_v0 {
631
632
export const VERSION = 'v0';
633
634
class Serializer implements IGalleryMcpServerDataSerializer {
635
636
private readonly galleryMcpServerDataSerializers: IGalleryMcpServerDataSerializer[] = [];
637
638
constructor() {
639
this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v0_1.SERIALIZER);
640
this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v2025_07_09.SERIALIZER);
641
}
642
643
public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {
644
for (const serializer of this.galleryMcpServerDataSerializers) {
645
const result = serializer.toRawGalleryMcpServerResult(input);
646
if (result) {
647
return result;
648
}
649
}
650
return undefined;
651
}
652
653
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
654
for (const serializer of this.galleryMcpServerDataSerializers) {
655
const result = serializer.toRawGalleryMcpServer(input);
656
if (result) {
657
return result;
658
}
659
}
660
return undefined;
661
}
662
}
663
664
export const SERIALIZER = new Serializer();
665
}
666
667
const DefaultPageSize = 50;
668
669
interface IQueryState {
670
readonly searchText?: string;
671
readonly cursor?: string;
672
readonly pageSize: number;
673
}
674
675
const DefaultQueryState: IQueryState = {
676
pageSize: DefaultPageSize,
677
};
678
679
class Query {
680
681
constructor(private state = DefaultQueryState) { }
682
683
get pageSize(): number { return this.state.pageSize; }
684
get searchText(): string | undefined { return this.state.searchText; }
685
get cursor(): string | undefined { return this.state.cursor; }
686
687
withPage(cursor: string, pageSize: number = this.pageSize): Query {
688
return new Query({ ...this.state, pageSize, cursor });
689
}
690
691
withSearchText(searchText: string | undefined): Query {
692
return new Query({ ...this.state, searchText });
693
}
694
}
695
696
export class McpGalleryService extends Disposable implements IMcpGalleryService {
697
698
_serviceBrand: undefined;
699
700
private galleryMcpServerDataSerializers: Map<string, IGalleryMcpServerDataSerializer>;
701
702
constructor(
703
@IRequestService private readonly requestService: IRequestService,
704
@IFileService private readonly fileService: IFileService,
705
@ILogService private readonly logService: ILogService,
706
@IMcpGalleryManifestService private readonly mcpGalleryManifestService: IMcpGalleryManifestService,
707
) {
708
super();
709
this.galleryMcpServerDataSerializers = new Map();
710
this.galleryMcpServerDataSerializers.set(McpServerSchemaVersion_v0.VERSION, McpServerSchemaVersion_v0.SERIALIZER);
711
this.galleryMcpServerDataSerializers.set(McpServerSchemaVersion_v0_1.VERSION, McpServerSchemaVersion_v0_1.SERIALIZER);
712
}
713
714
isEnabled(): boolean {
715
return this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Available;
716
}
717
718
async query(options?: IQueryOptions, token: CancellationToken = CancellationToken.None): Promise<IIterativePager<IGalleryMcpServer>> {
719
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
720
if (!mcpGalleryManifest) {
721
return {
722
firstPage: { items: [], hasMore: false },
723
getNextPage: async () => ({ items: [], hasMore: false })
724
};
725
}
726
727
let query = new Query();
728
if (options?.text) {
729
query = query.withSearchText(options.text.trim());
730
}
731
732
const { servers, metadata } = await this.queryGalleryMcpServers(query, mcpGalleryManifest, token);
733
734
let currentCursor = metadata.nextCursor;
735
return {
736
firstPage: { items: servers, hasMore: !!metadata.nextCursor },
737
getNextPage: async (ct: CancellationToken): Promise<IIterativePage<IGalleryMcpServer>> => {
738
if (ct.isCancellationRequested) {
739
throw new CancellationError();
740
}
741
if (!currentCursor) {
742
return { items: [], hasMore: false };
743
}
744
const { servers, metadata: nextMetadata } = await this.queryGalleryMcpServers(query.withPage(currentCursor).withSearchText(undefined), mcpGalleryManifest, ct);
745
currentCursor = nextMetadata.nextCursor;
746
return { items: servers, hasMore: !!nextMetadata.nextCursor };
747
}
748
};
749
}
750
751
async getMcpServersFromGallery(infos: { name: string; id?: string }[]): Promise<IGalleryMcpServer[]> {
752
const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
753
if (!mcpGalleryManifest) {
754
return [];
755
}
756
757
const mcpServers: IGalleryMcpServer[] = [];
758
await Promise.allSettled(infos.map(async info => {
759
const mcpServer = await this.getMcpServerByName(info, mcpGalleryManifest);
760
if (mcpServer) {
761
mcpServers.push(mcpServer);
762
}
763
}));
764
765
return mcpServers;
766
}
767
768
private async getMcpServerByName({ name, id }: { name: string; id?: string }, mcpGalleryManifest: IMcpGalleryManifest): Promise<IGalleryMcpServer | undefined> {
769
const mcpServerUrl = this.getLatestServerVersionUrl(name, mcpGalleryManifest);
770
if (mcpServerUrl) {
771
const mcpServer = await this.getMcpServer(mcpServerUrl);
772
if (mcpServer) {
773
return mcpServer;
774
}
775
}
776
777
const byNameUrl = this.getNamedServerUrl(name, mcpGalleryManifest);
778
if (byNameUrl) {
779
const mcpServer = await this.getMcpServer(byNameUrl);
780
if (mcpServer) {
781
return mcpServer;
782
}
783
}
784
785
const byIdUrl = id ? this.getServerIdUrl(id, mcpGalleryManifest) : undefined;
786
if (byIdUrl) {
787
const mcpServer = await this.getMcpServer(byIdUrl);
788
if (mcpServer) {
789
return mcpServer;
790
}
791
}
792
793
return undefined;
794
}
795
796
async getReadme(gallery: IGalleryMcpServer, token: CancellationToken): Promise<string> {
797
const readmeUrl = gallery.readmeUrl;
798
if (!readmeUrl) {
799
return Promise.resolve(localize('noReadme', 'No README available'));
800
}
801
802
const uri = URI.parse(readmeUrl);
803
if (uri.scheme === Schemas.file) {
804
try {
805
const content = await this.fileService.readFile(uri);
806
return content.value.toString();
807
} catch (error) {
808
this.logService.error(`Failed to read file from ${uri}: ${error}`);
809
}
810
}
811
812
if (uri.authority !== 'raw.githubusercontent.com') {
813
return new MarkdownString(localize('readme.viewInBrowser', "You can find information about this server [here]({0})", readmeUrl)).value;
814
}
815
816
const context = await this.requestService.request({
817
type: 'GET',
818
url: readmeUrl,
819
}, token);
820
821
const result = await asText(context);
822
if (!result) {
823
throw new Error(`Failed to fetch README from ${readmeUrl}`);
824
}
825
826
return result;
827
}
828
829
private toGalleryMcpServer(server: IRawGalleryMcpServer, manifest: IMcpGalleryManifest | null): IGalleryMcpServer {
830
let publisher = '';
831
let displayName = server.title;
832
833
if (server.githubInfo?.name) {
834
if (!displayName) {
835
displayName = server.githubInfo.name.split('-').map(s => s.toLowerCase() === 'mcp' ? 'MCP' : s.toLowerCase() === 'github' ? 'GitHub' : uppercaseFirstLetter(s)).join(' ');
836
}
837
publisher = server.githubInfo.nameWithOwner.split('/')[0];
838
} else {
839
const nameParts = server.name.split('/');
840
if (nameParts.length > 0) {
841
const domainParts = nameParts[0].split('.');
842
if (domainParts.length > 0) {
843
publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner
844
}
845
}
846
if (!displayName) {
847
displayName = nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' ');
848
}
849
}
850
851
if (server.githubInfo?.displayName) {
852
displayName = server.githubInfo.displayName;
853
}
854
855
let icon: { light: string; dark: string } | undefined;
856
857
if (server.githubInfo?.preferredImage) {
858
icon = {
859
light: server.githubInfo.preferredImage,
860
dark: server.githubInfo.preferredImage
861
};
862
}
863
864
else if (server.githubInfo?.ownerAvatarUrl) {
865
icon = {
866
light: server.githubInfo.ownerAvatarUrl,
867
dark: server.githubInfo.ownerAvatarUrl
868
};
869
}
870
871
else if (server.apicInfo?.['x-ms-icon']) {
872
icon = {
873
light: server.apicInfo['x-ms-icon'],
874
dark: server.apicInfo['x-ms-icon']
875
};
876
}
877
878
else if (server.icons && server.icons.length > 0) {
879
const lightIcon = server.icons.find(icon => icon.theme === 'light') ?? server.icons[0];
880
const darkIcon = server.icons.find(icon => icon.theme === 'dark') ?? lightIcon;
881
icon = {
882
light: lightIcon.src,
883
dark: darkIcon.src
884
};
885
}
886
887
const webUrl = manifest ? this.getWebUrl(server.name, manifest) : undefined;
888
const publisherUrl = manifest ? this.getPublisherUrl(publisher, manifest) : undefined;
889
890
return {
891
id: server.id,
892
name: server.name,
893
displayName,
894
galleryUrl: manifest?.url,
895
webUrl,
896
description: server.description,
897
status: server.status ?? GalleryMcpServerStatus.Active,
898
version: server.version,
899
isLatest: server.registryInfo?.isLatest ?? true,
900
publishDate: server.registryInfo?.publishedAt ? Date.parse(server.registryInfo.publishedAt) : undefined,
901
lastUpdated: server.githubInfo?.pushedAt ? Date.parse(server.githubInfo.pushedAt) : server.registryInfo?.updatedAt ? Date.parse(server.registryInfo.updatedAt) : undefined,
902
repositoryUrl: server.repository?.url,
903
readme: server.readme,
904
icon,
905
publisher,
906
publisherUrl,
907
license: server.githubInfo?.license,
908
starsCount: server.githubInfo?.stargazerCount,
909
topics: server.githubInfo?.topics,
910
configuration: {
911
packages: server.packages,
912
remotes: server.remotes
913
}
914
};
915
}
916
917
private async queryGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IGalleryMcpServersResult> {
918
const { servers, metadata } = await this.queryRawGalleryMcpServers(query, mcpGalleryManifest, token);
919
return {
920
servers: servers.map(item => this.toGalleryMcpServer(item, mcpGalleryManifest)),
921
metadata
922
};
923
}
924
925
private async queryRawGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IRawGalleryMcpServersResult> {
926
const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest);
927
if (!mcpGalleryUrl) {
928
return { servers: [], metadata: { count: 0 } };
929
}
930
931
const uri = URI.parse(mcpGalleryUrl);
932
if (uri.scheme === Schemas.file) {
933
try {
934
const content = await this.fileService.readFile(uri);
935
const data = content.value.toString();
936
return JSON.parse(data);
937
} catch (error) {
938
this.logService.error(`Failed to read file from ${uri}: ${error}`);
939
}
940
}
941
942
let url = `${mcpGalleryUrl}?limit=${query.pageSize}&version=latest`;
943
if (query.cursor) {
944
url += `&cursor=${query.cursor}`;
945
}
946
if (query.searchText) {
947
const text = encodeURIComponent(query.searchText);
948
url += `&search=${text}`;
949
}
950
951
const context = await this.requestService.request({
952
type: 'GET',
953
url,
954
}, token);
955
956
const data = await asJson(context);
957
958
if (!data) {
959
return { servers: [], metadata: { count: 0 } };
960
}
961
962
const result = this.serializeMcpServersResult(data, mcpGalleryManifest);
963
964
if (!result) {
965
throw new Error(`Failed to serialize MCP servers result from ${mcpGalleryUrl}`, data);
966
}
967
968
return result;
969
}
970
971
async getMcpServer(mcpServerUrl: string, mcpGalleryManifest?: IMcpGalleryManifest | null): Promise<IGalleryMcpServer | undefined> {
972
const context = await this.requestService.request({
973
type: 'GET',
974
url: mcpServerUrl,
975
}, CancellationToken.None);
976
977
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
978
return undefined;
979
}
980
981
const data = await asJson(context);
982
if (!data) {
983
return undefined;
984
}
985
986
if (!mcpGalleryManifest) {
987
mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();
988
}
989
mcpGalleryManifest = mcpGalleryManifest && mcpServerUrl.startsWith(mcpGalleryManifest.url) ? mcpGalleryManifest : null;
990
991
const server = this.serializeMcpServer(data, mcpGalleryManifest);
992
if (!server) {
993
throw new Error(`Failed to serialize MCP server from ${mcpServerUrl}`, data);
994
}
995
996
return this.toGalleryMcpServer(server, mcpGalleryManifest);
997
}
998
999
private serializeMcpServer(data: unknown, mcpGalleryManifest: IMcpGalleryManifest | null): IRawGalleryMcpServer | undefined {
1000
return this.getSerializer(mcpGalleryManifest)?.toRawGalleryMcpServer(data);
1001
}
1002
1003
private serializeMcpServersResult(data: unknown, mcpGalleryManifest: IMcpGalleryManifest | null): IRawGalleryMcpServersResult | undefined {
1004
return this.getSerializer(mcpGalleryManifest)?.toRawGalleryMcpServerResult(data);
1005
}
1006
1007
private getSerializer(mcpGalleryManifest: IMcpGalleryManifest | null): IGalleryMcpServerDataSerializer | undefined {
1008
const version = mcpGalleryManifest?.version ?? 'v0';
1009
return this.galleryMcpServerDataSerializers.get(version);
1010
}
1011
1012
private getNamedServerUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1013
const namedResourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerNamedResourceUri);
1014
if (!namedResourceUriTemplate) {
1015
return undefined;
1016
}
1017
return format2(namedResourceUriTemplate, { name });
1018
}
1019
1020
private getServerIdUrl(id: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1021
const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerIdUri);
1022
if (!resourceUriTemplate) {
1023
return undefined;
1024
}
1025
return format2(resourceUriTemplate, { id });
1026
}
1027
1028
private getLatestServerVersionUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1029
const latestVersionResourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerLatestVersionUri);
1030
if (!latestVersionResourceUriTemplate) {
1031
return undefined;
1032
}
1033
return format2(latestVersionResourceUriTemplate, { name: encodeURIComponent(name) });
1034
}
1035
1036
private getWebUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1037
const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerWebUri);
1038
if (!resourceUriTemplate) {
1039
return undefined;
1040
}
1041
return format2(resourceUriTemplate, { name });
1042
}
1043
1044
private getPublisherUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1045
const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.PublisherUriTemplate);
1046
if (!resourceUriTemplate) {
1047
return undefined;
1048
}
1049
return format2(resourceUriTemplate, { name });
1050
}
1051
1052
private getMcpGalleryUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined {
1053
return getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServersQueryService);
1054
}
1055
1056
}
1057
1058