Path: blob/main/src/vs/platform/mcp/common/mcpGalleryService.ts
5263 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../base/common/cancellation.js';6import { MarkdownString } from '../../../base/common/htmlContent.js';7import { Disposable } from '../../../base/common/lifecycle.js';8import { Schemas } from '../../../base/common/network.js';9import { format2, uppercaseFirstLetter } from '../../../base/common/strings.js';10import { URI } from '../../../base/common/uri.js';11import { localize } from '../../../nls.js';12import { IFileService } from '../../files/common/files.js';13import { ILogService } from '../../log/common/log.js';14import { asJson, asText, IRequestService } from '../../request/common/request.js';15import { GalleryMcpServerStatus, IGalleryMcpServer, IMcpGalleryService, IMcpServerArgument, IMcpServerInput, IMcpServerKeyValueInput, IMcpServerPackage, IQueryOptions, RegistryType, SseTransport, StreamableHttpTransport, Transport, TransportType } from './mcpManagement.js';16import { IMcpGalleryManifestService, McpGalleryManifestStatus, getMcpGalleryManifestResourceUri, McpGalleryResourceType, IMcpGalleryManifest } from './mcpGalleryManifest.js';17import { IIterativePager, IIterativePage } from '../../../base/common/paging.js';18import { CancellationError } from '../../../base/common/errors.js';19import { isObject, isString } from '../../../base/common/types.js';2021interface IMcpRegistryInfo {22readonly isLatest?: boolean;23readonly publishedAt?: string;24readonly updatedAt?: string;25}2627interface IGitHubInfo {28readonly name: string;29readonly nameWithOwner: string;30readonly displayName?: string;31readonly isInOrganization?: boolean;32readonly license?: string;33readonly opengraphImageUrl?: string;34readonly ownerAvatarUrl?: string;35readonly preferredImage?: string;36readonly primaryLanguage?: string;37readonly primaryLanguageColor?: string;38readonly pushedAt?: string;39readonly readme?: string;40readonly stargazerCount?: number;41readonly topics?: readonly string[];42readonly usesCustomOpengraphImage?: boolean;43}4445interface IAzureAPICenterInfo {46readonly 'x-ms-icon'?: string;47}4849interface IRawGalleryMcpServersMetadata {50readonly count: number;51readonly nextCursor?: string;52}5354interface IRawGalleryMcpServersResult {55readonly metadata: IRawGalleryMcpServersMetadata;56readonly servers: readonly IRawGalleryMcpServer[];57}5859interface IGalleryMcpServersResult {60readonly metadata: IRawGalleryMcpServersMetadata;61readonly servers: IGalleryMcpServer[];62}6364interface IRawGalleryMcpServer {65readonly name: string;66readonly description: string;67readonly version: string;68readonly id?: string;69readonly title?: string;70readonly repository?: {71readonly source: string;72readonly url: string;73readonly id?: string;74};75readonly readme?: string;76readonly icons?: readonly IRawGalleryMcpServerIcon[];77readonly status?: GalleryMcpServerStatus;78readonly websiteUrl?: string;79readonly createdAt?: string;80readonly updatedAt?: string;81readonly packages?: readonly IMcpServerPackage[];82readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;83readonly registryInfo?: IMcpRegistryInfo;84readonly githubInfo?: IGitHubInfo;85readonly apicInfo?: IAzureAPICenterInfo;86}8788interface IGalleryMcpServerDataSerializer {89toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined;90toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined;91}9293interface IRawGalleryMcpServerIcon {94readonly src: string;95readonly theme?: IconTheme;96readonly sizes?: string[];97readonly mimeType?: IconMimeType;98}99100const enum IconMimeType {101PNG = 'image/png',102JPEG = 'image/jpeg',103JPG = 'image/jpg',104SVG = 'image/svg+xml',105WEBP = 'image/webp',106}107108const enum IconTheme {109LIGHT = 'light',110DARK = 'dark',111}112113namespace McpServerSchemaVersion_v2025_07_09 {114115export const VERSION = 'v0-2025-07-09';116export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json`;117118interface RawGalleryMcpServerInput {119readonly description?: string;120readonly is_required?: boolean;121readonly format?: 'string' | 'number' | 'boolean' | 'filepath';122readonly value?: string;123readonly is_secret?: boolean;124readonly default?: string;125readonly choices?: readonly string[];126}127128interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {129readonly variables?: Record<string, RawGalleryMcpServerInput>;130}131132interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {133readonly type: 'positional';134readonly value_hint?: string;135readonly is_repeated?: boolean;136}137138interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {139readonly type: 'named';140readonly name: string;141readonly is_repeated?: boolean;142}143144interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {145readonly name: string;146readonly value?: string;147}148149type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;150151interface McpServerDeprecatedRemote {152readonly transport_type?: 'streamable' | 'sse';153readonly transport?: 'streamable' | 'sse';154readonly url: string;155readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;156}157158type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport | McpServerDeprecatedRemote>;159160type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;161162interface StdioTransport {163readonly type: 'stdio';164}165166interface StreamableHttpTransport {167readonly type: 'streamable-http' | 'sse';168readonly url: string;169readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;170}171172interface SseTransport {173readonly type: 'sse';174readonly url: string;175readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;176}177178interface RawGalleryMcpServerPackage {179readonly registry_name: string;180readonly name: string;181readonly registry_type: 'npm' | 'pypi' | 'docker-hub' | 'nuget' | 'remote' | 'mcpb';182readonly registry_base_url?: string;183readonly identifier: string;184readonly version: string;185readonly file_sha256?: string;186readonly transport?: RawGalleryTransport;187readonly package_arguments?: readonly RawGalleryMcpServerArgument[];188readonly runtime_hint?: string;189readonly runtime_arguments?: readonly RawGalleryMcpServerArgument[];190readonly environment_variables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;191}192193interface RawGalleryMcpServer {194readonly $schema: string;195readonly name: string;196readonly description: string;197readonly status?: 'active' | 'deprecated';198readonly repository?: {199readonly source: string;200readonly url: string;201readonly id?: string;202readonly readme?: string;203};204readonly version: string;205readonly website_url?: string;206readonly created_at: string;207readonly updated_at: string;208readonly packages?: readonly RawGalleryMcpServerPackage[];209readonly remotes?: RawGalleryMcpServerRemotes;210readonly _meta: {211readonly 'io.modelcontextprotocol.registry/official': {212readonly id: string;213readonly is_latest: boolean;214readonly published_at: string;215readonly updated_at: string;216readonly release_date?: string;217};218readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record<string, unknown>;219};220}221222interface RawGalleryMcpServersResult {223readonly metadata: {224readonly count: number;225readonly next_cursor?: string;226};227readonly servers: readonly RawGalleryMcpServer[];228}229230interface RawGitHubInfo {231readonly name: string;232readonly name_with_owner: string;233readonly display_name?: string;234readonly is_in_organization?: boolean;235readonly license?: string;236readonly opengraph_image_url?: string;237readonly owner_avatar_url?: string;238readonly primary_language?: string;239readonly primary_language_color?: string;240readonly pushed_at?: string;241readonly stargazer_count?: number;242readonly topics?: readonly string[];243readonly uses_custom_opengraph_image?: boolean;244}245246class Serializer implements IGalleryMcpServerDataSerializer {247248public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {249if (!input || typeof input !== 'object' || !Array.isArray((input as RawGalleryMcpServersResult).servers)) {250return undefined;251}252253const from = <RawGalleryMcpServersResult>input;254255const servers: IRawGalleryMcpServer[] = [];256for (const server of from.servers) {257const rawServer = this.toRawGalleryMcpServer(server);258if (!rawServer) {259return undefined;260}261servers.push(rawServer);262}263264return {265metadata: {266count: from.metadata.count ?? 0,267nextCursor: from.metadata?.next_cursor268},269servers270};271}272273public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {274if (!input || typeof input !== 'object') {275return undefined;276}277278const from = <RawGalleryMcpServer>input;279280if (281(!from.name || !isString(from.name))282|| (!from.description || !isString(from.description))283|| (!from.version || !isString(from.version))284) {285return undefined;286}287288if (from.$schema && from.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {289return undefined;290}291292const registryInfo = from._meta?.['io.modelcontextprotocol.registry/official'];293294function convertServerInput(input: RawGalleryMcpServerInput): IMcpServerInput {295return {296...input,297isRequired: input.is_required,298isSecret: input.is_secret,299};300}301302function convertVariables(variables: Record<string, RawGalleryMcpServerInput>): Record<string, IMcpServerInput> {303const result: Record<string, IMcpServerInput> = {};304for (const [key, value] of Object.entries(variables)) {305result[key] = convertServerInput(value);306}307return result;308}309310function convertServerArgument(arg: RawGalleryMcpServerArgument): IMcpServerArgument {311if (arg.type === 'positional') {312return {313...arg,314valueHint: arg.value_hint,315isRepeated: arg.is_repeated,316isRequired: arg.is_required,317isSecret: arg.is_secret,318variables: arg.variables ? convertVariables(arg.variables) : undefined,319};320}321return {322...arg,323isRepeated: arg.is_repeated,324isRequired: arg.is_required,325isSecret: arg.is_secret,326variables: arg.variables ? convertVariables(arg.variables) : undefined,327};328}329330function convertKeyValueInput(input: RawGalleryMcpServerKeyValueInput): IMcpServerKeyValueInput {331return {332...input,333isRequired: input.is_required,334isSecret: input.is_secret,335variables: input.variables ? convertVariables(input.variables) : undefined,336};337}338339function convertTransport(input: RawGalleryTransport): Transport {340switch (input.type) {341case 'stdio':342return {343type: TransportType.STDIO,344};345case 'streamable-http':346return {347type: TransportType.STREAMABLE_HTTP,348url: input.url,349headers: input.headers?.map(convertKeyValueInput),350};351case 'sse':352return {353type: TransportType.SSE,354url: input.url,355headers: input.headers?.map(convertKeyValueInput),356};357default:358return {359type: TransportType.STDIO,360};361}362}363364function convertRegistryType(input: string): RegistryType {365switch (input) {366case 'npm':367return RegistryType.NODE;368case 'docker':369case 'docker-hub':370case 'oci':371return RegistryType.DOCKER;372case 'pypi':373return RegistryType.PYTHON;374case 'nuget':375return RegistryType.NUGET;376case 'mcpb':377return RegistryType.MCPB;378default:379return RegistryType.NODE;380}381}382383const gitHubInfo: RawGitHubInfo | undefined = from._meta['io.modelcontextprotocol.registry/publisher-provided']?.github as RawGitHubInfo | undefined;384385return {386id: registryInfo.id,387name: from.name,388description: from.description,389repository: from.repository ? {390url: from.repository.url,391source: from.repository.source,392id: from.repository.id,393} : undefined,394readme: from.repository?.readme,395version: from.version,396createdAt: from.created_at,397updatedAt: from.updated_at,398packages: from.packages?.map<IMcpServerPackage>(p => ({399identifier: p.identifier ?? p.name,400registryType: convertRegistryType(p.registry_type ?? p.registry_name),401version: p.version,402fileSha256: p.file_sha256,403registryBaseUrl: p.registry_base_url,404transport: p.transport ? convertTransport(p.transport) : { type: TransportType.STDIO },405packageArguments: p.package_arguments?.map(convertServerArgument),406runtimeHint: p.runtime_hint,407runtimeArguments: p.runtime_arguments?.map(convertServerArgument),408environmentVariables: p.environment_variables?.map(convertKeyValueInput),409})),410remotes: from.remotes?.map(remote => {411const type = (<RawGalleryTransport>remote).type ?? (<McpServerDeprecatedRemote>remote).transport_type ?? (<McpServerDeprecatedRemote>remote).transport;412return {413type: type === TransportType.SSE ? TransportType.SSE : TransportType.STREAMABLE_HTTP,414url: remote.url,415headers: remote.headers?.map(convertKeyValueInput)416};417}),418registryInfo: {419isLatest: registryInfo.is_latest,420publishedAt: registryInfo.published_at,421updatedAt: registryInfo.updated_at,422},423githubInfo: gitHubInfo ? {424name: gitHubInfo.name,425nameWithOwner: gitHubInfo.name_with_owner,426displayName: gitHubInfo.display_name,427isInOrganization: gitHubInfo.is_in_organization,428license: gitHubInfo.license,429opengraphImageUrl: gitHubInfo.opengraph_image_url,430ownerAvatarUrl: gitHubInfo.owner_avatar_url,431primaryLanguage: gitHubInfo.primary_language,432primaryLanguageColor: gitHubInfo.primary_language_color,433pushedAt: gitHubInfo.pushed_at,434stargazerCount: gitHubInfo.stargazer_count,435topics: gitHubInfo.topics,436usesCustomOpengraphImage: gitHubInfo.uses_custom_opengraph_image437} : undefined438};439}440}441442export const SERIALIZER = new Serializer();443}444445namespace McpServerSchemaVersion_v0_1 {446447export const VERSION = 'v0.1';448449interface RawGalleryMcpServerInput {450readonly choices?: readonly string[];451readonly default?: string;452readonly description?: string;453readonly format?: 'string' | 'number' | 'boolean' | 'filepath';454readonly isRequired?: boolean;455readonly isSecret?: boolean;456readonly placeholder?: string;457readonly value?: string;458}459460interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {461readonly variables?: Record<string, RawGalleryMcpServerInput>;462}463464interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {465readonly type: 'positional';466readonly valueHint?: string;467readonly isRepeated?: boolean;468}469470interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {471readonly type: 'named';472readonly name: string;473readonly isRepeated?: boolean;474}475476interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {477readonly name: string;478}479480type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;481482type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport>;483484type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;485486interface StdioTransport {487readonly type: TransportType.STDIO;488}489490interface StreamableHttpTransport {491readonly type: TransportType.STREAMABLE_HTTP;492readonly url: string;493readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;494}495496interface SseTransport {497readonly type: TransportType.SSE;498readonly url: string;499readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;500}501502interface RawGalleryMcpServerPackage {503readonly identifier: string;504readonly registryType: RegistryType;505readonly transport: RawGalleryTransport;506readonly fileSha256?: string;507readonly environmentVariables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;508readonly packageArguments?: readonly RawGalleryMcpServerArgument[];509readonly registryBaseUrl?: string;510readonly runtimeArguments?: readonly RawGalleryMcpServerArgument[];511readonly runtimeHint?: string;512readonly version?: string;513}514515interface RawGalleryMcpServer {516readonly name: string;517readonly description: string;518readonly version: string;519readonly $schema: string;520readonly title?: string;521readonly icons?: IRawGalleryMcpServerIcon[];522readonly repository?: {523readonly source: string;524readonly url: string;525readonly subfolder?: string;526readonly id?: string;527};528readonly websiteUrl?: string;529readonly packages?: readonly RawGalleryMcpServerPackage[];530readonly remotes?: RawGalleryMcpServerRemotes;531readonly _meta?: {532readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record<string, unknown>;533} & IAzureAPICenterInfo;534}535536interface RawGalleryMcpServerInfo {537readonly server: RawGalleryMcpServer;538readonly _meta: {539readonly 'io.modelcontextprotocol.registry/official'?: {540readonly status: GalleryMcpServerStatus;541readonly isLatest: boolean;542readonly publishedAt: string;543readonly updatedAt?: string;544};545};546}547548interface RawGalleryMcpServersResult {549readonly metadata: {550readonly count: number;551readonly nextCursor?: string;552};553readonly servers: readonly RawGalleryMcpServerInfo[];554}555556class Serializer implements IGalleryMcpServerDataSerializer {557558public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {559if (!input || typeof input !== 'object' || !Array.isArray((input as RawGalleryMcpServersResult).servers)) {560return undefined;561}562563const from = <RawGalleryMcpServersResult>input;564565const servers: IRawGalleryMcpServer[] = [];566for (const server of from.servers) {567const rawServer = this.toRawGalleryMcpServer(server);568if (!rawServer) {569if (servers.length === 0) {570return undefined;571} else {572continue;573}574}575servers.push(rawServer);576}577578return {579metadata: from.metadata,580servers581};582}583584public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {585if (!input || typeof input !== 'object') {586return undefined;587}588589const from = <RawGalleryMcpServerInfo>input;590591if (592(!from.server || !isObject(from.server))593|| (!from.server.name || !isString(from.server.name))594|| (!from.server.description || !isString(from.server.description))595|| (!from.server.version || !isString(from.server.version))596) {597return undefined;598}599600const { 'io.modelcontextprotocol.registry/official': registryInfo, ...apicInfo } = from._meta;601const githubInfo = from.server._meta?.['io.modelcontextprotocol.registry/publisher-provided']?.github as IGitHubInfo | undefined;602603return {604name: from.server.name,605description: from.server.description,606version: from.server.version,607title: from.server.title,608repository: from.server.repository ? {609url: from.server.repository.url,610source: from.server.repository.source,611id: from.server.repository.id,612} : undefined,613readme: githubInfo?.readme,614icons: from.server.icons,615websiteUrl: from.server.websiteUrl,616packages: from.server.packages,617remotes: from.server.remotes,618status: registryInfo?.status,619registryInfo,620githubInfo,621apicInfo622};623}624}625626export const SERIALIZER = new Serializer();627}628629namespace McpServerSchemaVersion_v0 {630631export const VERSION = 'v0';632633class Serializer implements IGalleryMcpServerDataSerializer {634635private readonly galleryMcpServerDataSerializers: IGalleryMcpServerDataSerializer[] = [];636637constructor() {638this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v0_1.SERIALIZER);639this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v2025_07_09.SERIALIZER);640}641642public toRawGalleryMcpServerResult(input: unknown): IRawGalleryMcpServersResult | undefined {643for (const serializer of this.galleryMcpServerDataSerializers) {644const result = serializer.toRawGalleryMcpServerResult(input);645if (result) {646return result;647}648}649return undefined;650}651652public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {653for (const serializer of this.galleryMcpServerDataSerializers) {654const result = serializer.toRawGalleryMcpServer(input);655if (result) {656return result;657}658}659return undefined;660}661}662663export const SERIALIZER = new Serializer();664}665666const DefaultPageSize = 50;667668interface IQueryState {669readonly searchText?: string;670readonly cursor?: string;671readonly pageSize: number;672}673674const DefaultQueryState: IQueryState = {675pageSize: DefaultPageSize,676};677678class Query {679680constructor(private state = DefaultQueryState) { }681682get pageSize(): number { return this.state.pageSize; }683get searchText(): string | undefined { return this.state.searchText; }684get cursor(): string | undefined { return this.state.cursor; }685686withPage(cursor: string, pageSize: number = this.pageSize): Query {687return new Query({ ...this.state, pageSize, cursor });688}689690withSearchText(searchText: string | undefined): Query {691return new Query({ ...this.state, searchText });692}693}694695export class McpGalleryService extends Disposable implements IMcpGalleryService {696697_serviceBrand: undefined;698699private galleryMcpServerDataSerializers: Map<string, IGalleryMcpServerDataSerializer>;700701constructor(702@IRequestService private readonly requestService: IRequestService,703@IFileService private readonly fileService: IFileService,704@ILogService private readonly logService: ILogService,705@IMcpGalleryManifestService private readonly mcpGalleryManifestService: IMcpGalleryManifestService,706) {707super();708this.galleryMcpServerDataSerializers = new Map();709this.galleryMcpServerDataSerializers.set(McpServerSchemaVersion_v0.VERSION, McpServerSchemaVersion_v0.SERIALIZER);710this.galleryMcpServerDataSerializers.set(McpServerSchemaVersion_v0_1.VERSION, McpServerSchemaVersion_v0_1.SERIALIZER);711}712713isEnabled(): boolean {714return this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Available;715}716717async query(options?: IQueryOptions, token: CancellationToken = CancellationToken.None): Promise<IIterativePager<IGalleryMcpServer>> {718const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();719if (!mcpGalleryManifest) {720return {721firstPage: { items: [], hasMore: false },722getNextPage: async () => ({ items: [], hasMore: false })723};724}725726let query = new Query();727if (options?.text) {728query = query.withSearchText(options.text.trim());729}730731const { servers, metadata } = await this.queryGalleryMcpServers(query, mcpGalleryManifest, token);732733let currentCursor = metadata.nextCursor;734return {735firstPage: { items: servers, hasMore: !!metadata.nextCursor },736getNextPage: async (ct: CancellationToken): Promise<IIterativePage<IGalleryMcpServer>> => {737if (ct.isCancellationRequested) {738throw new CancellationError();739}740if (!currentCursor) {741return { items: [], hasMore: false };742}743const { servers, metadata: nextMetadata } = await this.queryGalleryMcpServers(query.withPage(currentCursor).withSearchText(undefined), mcpGalleryManifest, ct);744currentCursor = nextMetadata.nextCursor;745return { items: servers, hasMore: !!nextMetadata.nextCursor };746}747};748}749750async getMcpServersFromGallery(infos: { name: string; id?: string }[]): Promise<IGalleryMcpServer[]> {751const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();752if (!mcpGalleryManifest) {753return [];754}755756const mcpServers: IGalleryMcpServer[] = [];757await Promise.allSettled(infos.map(async info => {758const mcpServer = await this.getMcpServerByName(info, mcpGalleryManifest);759if (mcpServer) {760mcpServers.push(mcpServer);761}762}));763764return mcpServers;765}766767private async getMcpServerByName({ name, id }: { name: string; id?: string }, mcpGalleryManifest: IMcpGalleryManifest): Promise<IGalleryMcpServer | undefined> {768const mcpServerUrl = this.getLatestServerVersionUrl(name, mcpGalleryManifest);769if (mcpServerUrl) {770const mcpServer = await this.getMcpServer(mcpServerUrl);771if (mcpServer) {772return mcpServer;773}774}775776const byNameUrl = this.getNamedServerUrl(name, mcpGalleryManifest);777if (byNameUrl) {778const mcpServer = await this.getMcpServer(byNameUrl);779if (mcpServer) {780return mcpServer;781}782}783784const byIdUrl = id ? this.getServerIdUrl(id, mcpGalleryManifest) : undefined;785if (byIdUrl) {786const mcpServer = await this.getMcpServer(byIdUrl);787if (mcpServer) {788return mcpServer;789}790}791792return undefined;793}794795async getReadme(gallery: IGalleryMcpServer, token: CancellationToken): Promise<string> {796const readmeUrl = gallery.readmeUrl;797if (!readmeUrl) {798return Promise.resolve(localize('noReadme', 'No README available'));799}800801const uri = URI.parse(readmeUrl);802if (uri.scheme === Schemas.file) {803try {804const content = await this.fileService.readFile(uri);805return content.value.toString();806} catch (error) {807this.logService.error(`Failed to read file from ${uri}: ${error}`);808}809}810811if (uri.authority !== 'raw.githubusercontent.com') {812return new MarkdownString(localize('readme.viewInBrowser', "You can find information about this server [here]({0})", readmeUrl)).value;813}814815const context = await this.requestService.request({816type: 'GET',817url: readmeUrl,818}, token);819820const result = await asText(context);821if (!result) {822throw new Error(`Failed to fetch README from ${readmeUrl}`);823}824825return result;826}827828private toGalleryMcpServer(server: IRawGalleryMcpServer, manifest: IMcpGalleryManifest | null): IGalleryMcpServer {829let publisher = '';830let displayName = server.title;831832if (server.githubInfo?.name) {833if (!displayName) {834displayName = server.githubInfo.name.split('-').map(s => s.toLowerCase() === 'mcp' ? 'MCP' : s.toLowerCase() === 'github' ? 'GitHub' : uppercaseFirstLetter(s)).join(' ');835}836publisher = server.githubInfo.nameWithOwner.split('/')[0];837} else {838const nameParts = server.name.split('/');839if (nameParts.length > 0) {840const domainParts = nameParts[0].split('.');841if (domainParts.length > 0) {842publisher = domainParts[domainParts.length - 1]; // Always take the last part as owner843}844}845if (!displayName) {846displayName = nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' ');847}848}849850if (server.githubInfo?.displayName) {851displayName = server.githubInfo.displayName;852}853854let icon: { light: string; dark: string } | undefined;855856if (server.githubInfo?.preferredImage) {857icon = {858light: server.githubInfo.preferredImage,859dark: server.githubInfo.preferredImage860};861}862863else if (server.githubInfo?.ownerAvatarUrl) {864icon = {865light: server.githubInfo.ownerAvatarUrl,866dark: server.githubInfo.ownerAvatarUrl867};868}869870else if (server.apicInfo?.['x-ms-icon']) {871icon = {872light: server.apicInfo['x-ms-icon'],873dark: server.apicInfo['x-ms-icon']874};875}876877else if (server.icons && server.icons.length > 0) {878const lightIcon = server.icons.find(icon => icon.theme === 'light') ?? server.icons[0];879const darkIcon = server.icons.find(icon => icon.theme === 'dark') ?? lightIcon;880icon = {881light: lightIcon.src,882dark: darkIcon.src883};884}885886const webUrl = manifest ? this.getWebUrl(server.name, manifest) : undefined;887const publisherUrl = manifest ? this.getPublisherUrl(publisher, manifest) : undefined;888889return {890id: server.id,891name: server.name,892displayName,893galleryUrl: manifest?.url,894webUrl,895description: server.description,896status: server.status ?? GalleryMcpServerStatus.Active,897version: server.version,898isLatest: server.registryInfo?.isLatest ?? true,899publishDate: server.registryInfo?.publishedAt ? Date.parse(server.registryInfo.publishedAt) : undefined,900lastUpdated: server.githubInfo?.pushedAt ? Date.parse(server.githubInfo.pushedAt) : server.registryInfo?.updatedAt ? Date.parse(server.registryInfo.updatedAt) : undefined,901repositoryUrl: server.repository?.url,902readme: server.readme,903icon,904publisher,905publisherUrl,906license: server.githubInfo?.license,907starsCount: server.githubInfo?.stargazerCount,908topics: server.githubInfo?.topics,909configuration: {910packages: server.packages,911remotes: server.remotes912}913};914}915916private async queryGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IGalleryMcpServersResult> {917const { servers, metadata } = await this.queryRawGalleryMcpServers(query, mcpGalleryManifest, token);918return {919servers: servers.map(item => this.toGalleryMcpServer(item, mcpGalleryManifest)),920metadata921};922}923924private async queryRawGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise<IRawGalleryMcpServersResult> {925const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest);926if (!mcpGalleryUrl) {927return { servers: [], metadata: { count: 0 } };928}929930const uri = URI.parse(mcpGalleryUrl);931if (uri.scheme === Schemas.file) {932try {933const content = await this.fileService.readFile(uri);934const data = content.value.toString();935return JSON.parse(data);936} catch (error) {937this.logService.error(`Failed to read file from ${uri}: ${error}`);938}939}940941let url = `${mcpGalleryUrl}?limit=${query.pageSize}&version=latest`;942if (query.cursor) {943url += `&cursor=${query.cursor}`;944}945if (query.searchText) {946const text = encodeURIComponent(query.searchText);947url += `&search=${text}`;948}949950const context = await this.requestService.request({951type: 'GET',952url,953}, token);954955const data = await asJson(context);956957if (!data) {958return { servers: [], metadata: { count: 0 } };959}960961const result = this.serializeMcpServersResult(data, mcpGalleryManifest);962963if (!result) {964throw new Error(`Failed to serialize MCP servers result from ${mcpGalleryUrl}`, data);965}966967return result;968}969970async getMcpServer(mcpServerUrl: string, mcpGalleryManifest?: IMcpGalleryManifest | null): Promise<IGalleryMcpServer | undefined> {971const context = await this.requestService.request({972type: 'GET',973url: mcpServerUrl,974}, CancellationToken.None);975976if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {977return undefined;978}979980const data = await asJson(context);981if (!data) {982return undefined;983}984985if (!mcpGalleryManifest) {986mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest();987}988mcpGalleryManifest = mcpGalleryManifest && mcpServerUrl.startsWith(mcpGalleryManifest.url) ? mcpGalleryManifest : null;989990const server = this.serializeMcpServer(data, mcpGalleryManifest);991if (!server) {992throw new Error(`Failed to serialize MCP server from ${mcpServerUrl}`, data);993}994995return this.toGalleryMcpServer(server, mcpGalleryManifest);996}997998private serializeMcpServer(data: unknown, mcpGalleryManifest: IMcpGalleryManifest | null): IRawGalleryMcpServer | undefined {999return this.getSerializer(mcpGalleryManifest)?.toRawGalleryMcpServer(data);1000}10011002private serializeMcpServersResult(data: unknown, mcpGalleryManifest: IMcpGalleryManifest | null): IRawGalleryMcpServersResult | undefined {1003return this.getSerializer(mcpGalleryManifest)?.toRawGalleryMcpServerResult(data);1004}10051006private getSerializer(mcpGalleryManifest: IMcpGalleryManifest | null): IGalleryMcpServerDataSerializer | undefined {1007const version = mcpGalleryManifest?.version ?? 'v0';1008return this.galleryMcpServerDataSerializers.get(version);1009}10101011private getNamedServerUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1012const namedResourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerNamedResourceUri);1013if (!namedResourceUriTemplate) {1014return undefined;1015}1016return format2(namedResourceUriTemplate, { name });1017}10181019private getServerIdUrl(id: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1020const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerIdUri);1021if (!resourceUriTemplate) {1022return undefined;1023}1024return format2(resourceUriTemplate, { id });1025}10261027private getLatestServerVersionUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1028const latestVersionResourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerLatestVersionUri);1029if (!latestVersionResourceUriTemplate) {1030return undefined;1031}1032return format2(latestVersionResourceUriTemplate, { name: encodeURIComponent(name) });1033}10341035private getWebUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1036const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerWebUri);1037if (!resourceUriTemplate) {1038return undefined;1039}1040return format2(resourceUriTemplate, { name });1041}10421043private getPublisherUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1044const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.PublisherUriTemplate);1045if (!resourceUriTemplate) {1046return undefined;1047}1048return format2(resourceUriTemplate, { name });1049}10501051private getMcpGalleryUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined {1052return getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServersQueryService);1053}10541055}105610571058