Path: blob/main/extensions/copilot/src/extension/mcp/vscode-node/nuget.ts
13401 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 * as fs from 'fs/promises';6import * as os from 'os';7import path from 'path';8import { l10n } from 'vscode';9import { ILogService } from '../../../platform/log/common/logService';10import { IFetcherService } from '../../../platform/networking/common/fetcherService';11import { IStringDictionary } from '../../../util/vs/base/common/collections';12import { randomPath } from '../../../util/vs/base/common/extpath';13import { isObject } from '../../../util/vs/base/common/types';14import { ValidatePackageErrorType, ValidatePackageResult } from './commands';15import { CommandExecutor, ICommandExecutor } from './util';1617interface NuGetServiceIndexResponse {18resources?: Array<{ '@id': string; '@type': string }>;19}2021interface DotnetPackageSearchOutput {22searchResult?: Array<SourceResult>;23}2425interface SourceResult {26sourceName: string;27packages?: Array<LatestPackageResult>;28}2930interface LatestPackageResult {31id: string;32latestVersion: string;33owners?: string;34}3536interface DotnetCli {37command: string;38args: Array<string>;39}4041const MCP_SERVER_SCHEMA_2025_07_09_GH = 'https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json';4243export class NuGetMcpSetup {44constructor(45public readonly logService: ILogService,46public readonly fetcherService: IFetcherService,4748public readonly commandExecutor: ICommandExecutor = new CommandExecutor(),4950public readonly dotnet: DotnetCli = { command: 'dotnet', args: [] },5152// use NuGet.org central registry53// see https://github.com/microsoft/vscode/issues/259901 for future options54public readonly source: string = 'https://api.nuget.org/v3/index.json'55) { }5657async getNuGetPackageMetadata(id: string): Promise<ValidatePackageResult> {58// use the home directory, which is the default for MCP servers59// see https://github.com/microsoft/vscode/issues/259901 for future options60const cwd = os.homedir();6162// check for .NET CLI version for a quick "is dotnet installed?" check63let dotnetVersion;64try {65dotnetVersion = await this.getDotnetVersion(cwd);66} catch (error) {67const errorCode = error.hasOwnProperty('code') ? String((error as any).code) : undefined;68if (errorCode === 'ENOENT') {69return {70state: 'error',71error: l10n.t("The '{0}' command was not found. .NET SDK 10 or newer must be installed and available in PATH.", this.dotnet.command),72errorType: ValidatePackageErrorType.MissingCommand,73helpUri: 'https://aka.ms/vscode-mcp-install/dotnet',74helpUriLabel: l10n.t("Install .NET SDK"),75};76} else {77throw error;78}79}8081// dnx is used for running .NET MCP servers and it was shipped with .NET 1082const dotnetMajorVersion = parseInt(dotnetVersion.split('.')[0]);83if (dotnetMajorVersion < 10) {84return {85state: 'error',86error: l10n.t("The installed .NET SDK must be version 10 or newer. Found {0}.", dotnetVersion),87errorType: ValidatePackageErrorType.BadCommandVersion,88helpUri: 'https://aka.ms/vscode-mcp-install/dotnet',89helpUriLabel: l10n.t("Update .NET SDK"),90};91}9293// check if the package exists, using .NET CLI94const latest = await this.getLatestPackageVersion(cwd, id);95if (!latest) {96return {97state: 'error',98errorType: ValidatePackageErrorType.NotFound,99error: l10n.t("Package {0} does not exist on NuGet.org.", id)100};101}102103// read the package readme from NuGet.org, using the HTTP API104const readme = await this.getPackageReadmeFromNuGetOrgAsync(latest.id, latest.version);105106return {107state: 'ok',108publisher: latest.owners ?? 'unknown',109name: latest.id,110version: latest.version,111readme,112getMcpServer: async (installConsent) => {113// getting the server.json downloads the package, so wait for consent114await installConsent;115const manifest = await this.getServerManifest(latest.id, latest.version);116return mapServerJsonToMcpServer(manifest, RegistryType.NUGET);117},118};119}120121async getServerManifest(id: string, version: string): Promise<string | undefined> {122this.logService.info(`Reading .mcp/server.json from NuGet package ${id}@${version}.`);123const installDir = randomPath(os.tmpdir(), 'vscode-nuget-mcp');124try {125// perform a local tool install using the .NET CLI126// this warms the cache (user packages folder) so dnx will be fast127// this also makes the server.json available which will be mapped to VS Code MCP config128await fs.mkdir(installDir, { recursive: true });129130// the cwd must be the install directory or a child directory for local tool install to work131const cwd = installDir;132133const packagesDir = await this.getGlobalPackagesPath(id, version, cwd);134if (!packagesDir) { return undefined; }135136// explicitly create a tool manifest in the off chance one already exists in a parent directory137const createManifestSuccess = await this.createToolManifest(id, version, cwd);138if (!createManifestSuccess) { return undefined; }139140const localInstallSuccess = await this.installLocalTool(id, version, cwd);141if (!localInstallSuccess) { return undefined; }142143return await this.readServerManifest(packagesDir, id, version);144} catch (e) {145this.logService.warn(`146Failed to install NuGet package ${id}@${version}. Proceeding without server.json.147Error: ${e}`);148} finally {149try {150await fs.rm(installDir, { recursive: true, force: true });151} catch (e) {152this.logService.warn(`Failed to clean up temporary .NET tool install directory ${installDir}.153Error: ${e}`);154}155}156}157158async getDotnetVersion(cwd: string): Promise<string> {159const args = this.dotnet.args.concat(['--version']);160const result = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);161const version = result.stdout.trim();162if (result.exitCode !== 0 || !version) {163this.logService.warn(`Failed to check for .NET version while checking if a NuGet MCP server exists.164stdout: ${result.stdout}165stderr: ${result.stderr}`);166throw new Error(`Failed to check for .NET version using '${this.dotnet.command} --version'.`);167}168169return version;170}171172async getLatestPackageVersion(cwd: string, id: string): Promise<{ id: string; version: string; owners?: string } | undefined> {173// we don't use --exact-match here because it does not return owner information on NuGet.org174const args = this.dotnet.args.concat(['package', 'search', id, '--source', this.source, '--prerelease', '--format', 'json']);175const searchResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);176const searchData: DotnetPackageSearchOutput = JSON.parse(searchResult.stdout.trim());177for (const result of searchData.searchResult ?? []) {178for (const pkg of result.packages ?? []) {179if (pkg.id.toUpperCase() === id.toUpperCase()) {180return { id: pkg.id, version: pkg.latestVersion, owners: pkg.owners };181}182}183}184}185186async getPackageReadmeFromNuGetOrgAsync(id: string, version: string): Promise<string | undefined> {187try {188const sourceUrl = URL.parse(this.source);189if (sourceUrl?.protocol !== 'https:' || !sourceUrl.pathname.endsWith('.json')) {190this.logService.warn(`NuGet package source is not an HTTPS V3 source URL. Cannot fetch a readme for ${id}@${version}.`);191return;192}193194// download the service index to locate services195// https://learn.microsoft.com/en-us/nuget/api/service-index196const serviceIndexResponse = await this.fetcherService.fetch(this.source, { method: 'GET', callSite: 'mcp-nuget-service-index' });197if (serviceIndexResponse.status !== 200) {198this.logService.warn(`Unable to read the service index for NuGet.org while fetching readme for ${id}@${version}.199HTTP status: ${serviceIndexResponse.status}`);200return;201}202203const serviceIndex = await serviceIndexResponse.json() as NuGetServiceIndexResponse;204205// try to fetch the package readme using the URL template206// https://learn.microsoft.com/en-us/nuget/api/readme-template-resource207const readmeTemplate = serviceIndex.resources?.find(resource => resource['@type'] === 'ReadmeUriTemplate/6.13.0')?.['@id'];208if (!readmeTemplate) {209this.logService.warn(`No readme URL template found for ${id}@${version} on NuGet.org.`);210return;211}212213const readmeUrl = readmeTemplate214.replace('{lower_id}', encodeURIComponent(id.toLowerCase()))215.replace('{lower_version}', encodeURIComponent(version.toLowerCase()));216const readmeResponse = await this.fetcherService.fetch(readmeUrl, { method: 'GET', callSite: 'mcp-nuget-readme' });217if (readmeResponse.status === 200) {218return readmeResponse.text();219} else if (readmeResponse.status === 404) {220this.logService.info(`No package readme exists for ${id}@${version} on NuGet.org.`);221} else {222this.logService.warn(`Failed to read package readme for ${id}@${version} from NuGet.org.223HTTP status: ${readmeResponse.status}`);224}225} catch (error) {226this.logService.warn(`Failed to read package readme for ${id}@${version} from NuGet.org.227Error: ${error}`);228}229}230231async getGlobalPackagesPath(id: string, version: string, cwd: string): Promise<string | undefined> {232const args = this.dotnet.args.concat(['nuget', 'locals', 'global-packages', '--list', '--force-english-output']);233const globalPackagesResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);234235if (globalPackagesResult.exitCode !== 0) {236this.logService.warn(`Failed to discover the NuGet global packages folder. Proceeding without server.json for ${id}@${version}.237stdout: ${globalPackagesResult.stdout}238stderr: ${globalPackagesResult.stderr}`);239return undefined;240}241242// output looks like:243// global-packages: C:\Users\username\.nuget\packages\244return globalPackagesResult.stdout.trim().split(' ', 2).at(-1)?.trim();245}246247async createToolManifest(id: string, version: string, cwd: string): Promise<boolean> {248const args = this.dotnet.args.concat(['new', 'tool-manifest']);249const result = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);250251if (result.exitCode !== 0) {252this.logService.warn(`Failed to create tool manifest.Proceeding without server.json for ${id}@${version}.253stdout: ${result.stdout}254stderr: ${result.stderr}`);255return false;256}257258return true;259}260261async installLocalTool(id: string, version: string, cwd: string): Promise<boolean> {262const args = this.dotnet.args.concat(['tool', 'install', `${id}@${version}`, '--source', this.source, '--local', '--create-manifest-if-needed']);263const installResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);264265if (installResult.exitCode !== 0) {266this.logService.warn(`Failed to install local tool ${id} @${version}. Proceeding without server.json for ${id}@${version}.267stdout: ${installResult.stdout}268stderr: ${installResult.stderr}`);269return false;270}271272return true;273}274275prepareServerJson(manifest: any, id: string, version: string): any {276// Force the ID and version of matching NuGet package in the server.json to the one we installed.277// This handles cases where the server.json in the package is stale.278// The ID should match generally, but we'll protect against unexpected package IDs.279// We handle old and new schema formats:280// - https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json (only hosted in GitHub)281// - https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json (had several breaking changes over time)282// - https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json283if (manifest?.packages) {284for (const pkg of manifest.packages) {285if (!pkg) { continue; }286const registryType = pkg.registryType ?? pkg.registry_type ?? pkg.registry_name;287if (registryType === 'nuget') {288if (pkg.name && pkg.name !== id) {289this.logService.warn(`Package name mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.name}.`);290pkg.name = id;291}292293if (pkg.identifier && pkg.identifier !== id) {294this.logService.warn(`Package identifier mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.identifier}.`);295pkg.identifier = id;296}297298if (pkg.version !== version) {299this.logService.warn(`Package version mismatch in NuGet.mcp / server.json: expected ${version}, found ${pkg.version}.`);300pkg.version = version;301}302}303}304}305306// the original .NET MCP server project template used a schema URL that is deprecated307if (manifest['$schema'] === MCP_SERVER_SCHEMA_2025_07_09_GH || !manifest['$schema']) {308manifest['$schema'] = McpServerSchemaVersion_v2025_07_09.SCHEMA;309}310311// add missing properties to improve mapping312if (!manifest.name) { manifest.name = id; }313if (!manifest.description) { manifest.description = id; }314if (!manifest.version) { manifest.version = version; }315316return manifest;317}318319async readServerManifest(packagesDir: string, id: string, version: string): Promise<string | undefined> {320const serverJsonPath = path.join(packagesDir, id.toLowerCase(), version.toLowerCase(), '.mcp', 'server.json');321try {322await fs.access(serverJsonPath, fs.constants.R_OK);323} catch {324this.logService.info(`No server.json found at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);325return undefined;326}327328const json = await fs.readFile(serverJsonPath, 'utf8');329let manifest;330try {331manifest = JSON.parse(json);332} catch {333this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);334return undefined;335}336if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {337this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);338return undefined;339}340341return this.prepareServerJson(manifest, id, version);342}343}344345export function mapServerJsonToMcpServer(input: unknown, registryType: RegistryType): Omit<IInstallableMcpServer, 'name'> | undefined {346let data: any = input;347348if (!data || typeof data !== 'object' || typeof data.$schema !== 'string') {349return undefined;350}351352// starting from 2025-09-29, the server.json is wrapped in a "server" property353if (data.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {354data = { server: data };355}356357const raw = McpServerSchemaVersion_v0.SERIALIZER.toRawGalleryMcpServer(data);358if (!raw) {359return undefined;360}361362const utility = new McpMappingUtility();363const result = utility.getMcpServerConfigurationFromManifest(raw, registryType);364return result.mcpServerConfiguration;365}366367// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpGalleryService.ts368369interface IGalleryMcpServerDataSerializer {370toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined;371}372373interface IRawGalleryMcpServer {374readonly packages?: readonly IMcpServerPackage[];375readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;376}377378export namespace McpServerSchemaVersion_v2025_07_09 {379380export const VERSION = 'v0-2025-07-09';381export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json`;382383interface RawGalleryMcpServerInput {384readonly description?: string;385readonly is_required?: boolean;386readonly format?: 'string' | 'number' | 'boolean' | 'filepath';387readonly value?: string;388readonly is_secret?: boolean;389readonly default?: string;390readonly choices?: readonly string[];391}392393interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {394readonly variables?: Record<string, RawGalleryMcpServerInput>;395}396397interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {398readonly type: 'positional';399readonly value_hint?: string;400readonly is_repeated?: boolean;401}402403interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {404readonly type: 'named';405readonly name: string;406readonly is_repeated?: boolean;407}408409interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {410readonly name: string;411readonly value?: string;412}413414type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;415416interface McpServerDeprecatedRemote {417readonly transport_type?: 'streamable' | 'sse';418readonly transport?: 'streamable' | 'sse';419readonly url: string;420readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;421}422423type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport | McpServerDeprecatedRemote>;424425type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;426427interface StdioTransport {428readonly type: 'stdio';429}430431interface StreamableHttpTransport {432readonly type: 'streamable-http' | 'sse';433readonly url: string;434readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;435}436437interface SseTransport {438readonly type: 'sse';439readonly url: string;440readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;441}442443interface RawGalleryMcpServerPackage {444readonly registry_name: string;445readonly name: string;446readonly registry_type: 'npm' | 'pypi' | 'docker-hub' | 'nuget' | 'remote' | 'mcpb';447readonly registry_base_url?: string;448readonly identifier: string;449readonly version: string;450readonly file_sha256?: string;451readonly transport?: RawGalleryTransport;452readonly package_arguments?: readonly RawGalleryMcpServerArgument[];453readonly runtime_hint?: string;454readonly runtime_arguments?: readonly RawGalleryMcpServerArgument[];455readonly environment_variables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;456}457458interface RawGalleryMcpServer {459readonly $schema: string;460readonly packages?: readonly RawGalleryMcpServerPackage[];461readonly remotes?: RawGalleryMcpServerRemotes;462}463464class Serializer implements IGalleryMcpServerDataSerializer {465466public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {467if (!input || typeof input !== 'object') {468return undefined;469}470471const from = <RawGalleryMcpServer>input;472473if (from.$schema && from.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {474return undefined;475}476477function convertServerInput(input: RawGalleryMcpServerInput): IMcpServerInput {478return {479...input,480isRequired: input.is_required,481isSecret: input.is_secret,482};483}484485function convertVariables(variables: Record<string, RawGalleryMcpServerInput>): Record<string, IMcpServerInput> {486const result: Record<string, IMcpServerInput> = {};487for (const [key, value] of Object.entries(variables)) {488result[key] = convertServerInput(value);489}490return result;491}492493function convertServerArgument(arg: RawGalleryMcpServerArgument): IMcpServerArgument {494if (arg.type === 'positional') {495return {496...arg,497valueHint: arg.value_hint,498isRepeated: arg.is_repeated,499isRequired: arg.is_required,500isSecret: arg.is_secret,501variables: arg.variables ? convertVariables(arg.variables) : undefined,502};503}504return {505...arg,506isRepeated: arg.is_repeated,507isRequired: arg.is_required,508isSecret: arg.is_secret,509variables: arg.variables ? convertVariables(arg.variables) : undefined,510};511}512513function convertKeyValueInput(input: RawGalleryMcpServerKeyValueInput): IMcpServerKeyValueInput {514return {515...input,516isRequired: input.is_required,517isSecret: input.is_secret,518variables: input.variables ? convertVariables(input.variables) : undefined,519};520}521522function convertTransport(input: RawGalleryTransport): Transport {523switch (input.type) {524case 'stdio':525return {526type: TransportType.STDIO,527};528case 'streamable-http':529return {530type: TransportType.STREAMABLE_HTTP,531url: input.url,532headers: input.headers?.map(convertKeyValueInput),533};534case 'sse':535return {536type: TransportType.SSE,537url: input.url,538headers: input.headers?.map(convertKeyValueInput),539};540default:541return {542type: TransportType.STDIO,543};544}545}546547function convertRegistryType(input: string): RegistryType {548switch (input) {549case 'npm':550return RegistryType.NODE;551case 'docker':552case 'docker-hub':553case 'oci':554return RegistryType.DOCKER;555case 'pypi':556return RegistryType.PYTHON;557case 'nuget':558return RegistryType.NUGET;559case 'mcpb':560return RegistryType.MCPB;561default:562return RegistryType.NODE;563}564}565566return {567packages: from.packages?.map<IMcpServerPackage>(p => ({568identifier: p.identifier ?? p.name,569registryType: convertRegistryType(p.registry_type ?? p.registry_name),570version: p.version,571fileSha256: p.file_sha256,572registryBaseUrl: p.registry_base_url,573transport: p.transport ? convertTransport(p.transport) : { type: TransportType.STDIO },574packageArguments: p.package_arguments?.map(convertServerArgument),575runtimeHint: p.runtime_hint,576runtimeArguments: p.runtime_arguments?.map(convertServerArgument),577environmentVariables: p.environment_variables?.map(convertKeyValueInput),578})),579remotes: from.remotes?.map(remote => {580const type = (<RawGalleryTransport>remote).type ?? (<McpServerDeprecatedRemote>remote).transport_type ?? (<McpServerDeprecatedRemote>remote).transport;581return {582type: type === TransportType.SSE ? TransportType.SSE : TransportType.STREAMABLE_HTTP,583url: remote.url,584headers: remote.headers?.map(convertKeyValueInput)585};586}),587};588}589}590591export const SERIALIZER = new Serializer();592}593594namespace McpServerSchemaVersion_v0_1 {595596export const VERSION = 'v0.1';597export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json`;598599interface RawGalleryMcpServerInput {600readonly choices?: readonly string[];601readonly default?: string;602readonly description?: string;603readonly format?: 'string' | 'number' | 'boolean' | 'filepath';604readonly isRequired?: boolean;605readonly isSecret?: boolean;606readonly placeholder?: string;607readonly value?: string;608}609610interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {611readonly variables?: Record<string, RawGalleryMcpServerInput>;612}613614interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {615readonly type: 'positional';616readonly valueHint?: string;617readonly isRepeated?: boolean;618}619620interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {621readonly type: 'named';622readonly name: string;623readonly isRepeated?: boolean;624}625626interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {627readonly name: string;628}629630type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;631632type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport>;633634type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;635636interface StdioTransport {637readonly type: TransportType.STDIO;638}639640interface StreamableHttpTransport {641readonly type: TransportType.STREAMABLE_HTTP;642readonly url: string;643readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;644}645646interface SseTransport {647readonly type: TransportType.SSE;648readonly url: string;649readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;650}651652interface RawGalleryMcpServerPackage {653readonly registryType: RegistryType;654readonly identifier: string;655readonly version: string;656readonly transport: RawGalleryTransport;657readonly registryBaseUrl?: string;658readonly fileSha256?: string;659readonly packageArguments?: readonly RawGalleryMcpServerArgument[];660readonly runtimeHint?: string;661readonly runtimeArguments?: readonly RawGalleryMcpServerArgument[];662readonly environmentVariables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;663}664665interface RawGalleryMcpServer {666readonly $schema: string;667readonly packages?: readonly RawGalleryMcpServerPackage[];668readonly remotes?: RawGalleryMcpServerRemotes;669}670671interface RawGalleryMcpServerInfo {672readonly server: RawGalleryMcpServer;673}674675class Serializer implements IGalleryMcpServerDataSerializer {676677public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {678if (!input || typeof input !== 'object') {679return undefined;680}681682const from = <RawGalleryMcpServerInfo>input;683684if (685(!from.server || !isObject(from.server))686) {687return undefined;688}689690if (from.server.$schema && from.server.$schema !== McpServerSchemaVersion_v0_1.SCHEMA) {691return undefined;692}693694return {695packages: from.server.packages,696remotes: from.server.remotes,697};698}699}700701export const SERIALIZER = new Serializer();702}703704export namespace McpServerSchemaVersion_v0 {705706export const VERSION = 'v0';707708class Serializer implements IGalleryMcpServerDataSerializer {709710private readonly galleryMcpServerDataSerializers: IGalleryMcpServerDataSerializer[] = [];711712constructor() {713this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v0_1.SERIALIZER);714this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v2025_07_09.SERIALIZER);715}716717public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {718for (const serializer of this.galleryMcpServerDataSerializers) {719const result = serializer.toRawGalleryMcpServer(input);720if (result) {721return result;722}723}724return undefined;725}726}727728export const SERIALIZER = new Serializer();729}730731732export interface IMcpServerInput {733readonly description?: string;734readonly isRequired?: boolean;735readonly format?: 'string' | 'number' | 'boolean' | 'filepath';736readonly value?: string;737readonly isSecret?: boolean;738readonly default?: string;739readonly choices?: readonly string[];740}741742export interface IMcpServerVariableInput extends IMcpServerInput {743readonly variables?: Record<string, IMcpServerInput>;744}745746export interface IMcpServerPositionalArgument extends IMcpServerVariableInput {747readonly type: 'positional';748readonly valueHint?: string;749readonly isRepeated?: boolean;750}751752export interface IMcpServerNamedArgument extends IMcpServerVariableInput {753readonly type: 'named';754readonly name: string;755readonly isRepeated?: boolean;756}757758export interface IMcpServerKeyValueInput extends IMcpServerVariableInput {759readonly name: string;760readonly value?: string;761}762763export type IMcpServerArgument = IMcpServerPositionalArgument | IMcpServerNamedArgument;764765export const enum RegistryType {766NODE = 'npm',767PYTHON = 'pypi',768DOCKER = 'oci',769NUGET = 'nuget',770MCPB = 'mcpb',771REMOTE = 'remote'772}773774export const enum TransportType {775STDIO = 'stdio',776STREAMABLE_HTTP = 'streamable-http',777SSE = 'sse'778}779780export interface StdioTransport {781readonly type: TransportType.STDIO;782}783784export interface StreamableHttpTransport {785readonly type: TransportType.STREAMABLE_HTTP;786readonly url: string;787readonly headers?: ReadonlyArray<IMcpServerKeyValueInput>;788}789790export interface SseTransport {791readonly type: TransportType.SSE;792readonly url: string;793readonly headers?: ReadonlyArray<IMcpServerKeyValueInput>;794}795796export type Transport = StdioTransport | StreamableHttpTransport | SseTransport;797798export interface IMcpServerPackage {799readonly registryType: RegistryType;800readonly identifier: string;801readonly version: string;802readonly transport?: Transport;803readonly registryBaseUrl?: string;804readonly fileSha256?: string;805readonly packageArguments?: readonly IMcpServerArgument[];806readonly runtimeHint?: string;807readonly runtimeArguments?: readonly IMcpServerArgument[];808readonly environmentVariables?: ReadonlyArray<IMcpServerKeyValueInput>;809}810811export interface IGalleryMcpServerConfiguration {812readonly packages?: readonly IMcpServerPackage[];813readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;814}815816export const enum GalleryMcpServerStatus {817Active = 'active',818Deprecated = 'deprecated'819}820821export interface IInstallableMcpServer {822readonly name: string;823readonly config: IMcpServerConfiguration;824readonly inputs?: IMcpServerVariable[];825}826827export type McpServerConfiguration = Omit<IInstallableMcpServer, 'name'>;828export interface McpServerConfigurationParseResult {829readonly mcpServerConfiguration: McpServerConfiguration;830readonly notices: string[];831}832833834// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpManagementService.ts835836export class McpMappingUtility {837getMcpServerConfigurationFromManifest(manifest: IGalleryMcpServerConfiguration, packageType: RegistryType): McpServerConfigurationParseResult {838839// remote840if (packageType === RegistryType.REMOTE && manifest.remotes?.length) {841const { inputs, variables } = this.processKeyValueInputs(manifest.remotes[0].headers ?? []);842return {843mcpServerConfiguration: {844config: {845type: McpServerType.REMOTE,846url: manifest.remotes[0].url,847headers: Object.keys(inputs).length ? inputs : undefined,848},849inputs: variables.length ? variables : undefined,850},851notices: [],852};853}854855// local856const serverPackage = manifest.packages?.find(p => p.registryType === packageType) ?? manifest.packages?.[0];857if (!serverPackage) {858throw new Error(`No server package found`);859}860861const args: string[] = [];862const inputs: IMcpServerVariable[] = [];863const env: Record<string, string> = {};864const notices: string[] = [];865866if (serverPackage.registryType === RegistryType.DOCKER) {867args.push('run');868args.push('-i');869args.push('--rm');870}871872if (serverPackage.runtimeArguments?.length) {873const result = this.processArguments(serverPackage.runtimeArguments ?? []);874args.push(...result.args);875inputs.push(...result.variables);876notices.push(...result.notices);877}878879if (serverPackage.environmentVariables?.length) {880const { inputs: envInputs, variables: envVariables, notices: envNotices } = this.processKeyValueInputs(serverPackage.environmentVariables ?? []);881inputs.push(...envVariables);882notices.push(...envNotices);883for (const [name, value] of Object.entries(envInputs)) {884env[name] = value;885if (serverPackage.registryType === RegistryType.DOCKER) {886args.push('-e');887args.push(name);888}889}890}891892switch (serverPackage.registryType) {893case RegistryType.NODE:894args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);895break;896case RegistryType.PYTHON:897args.push(serverPackage.version ? `${serverPackage.identifier}==${serverPackage.version}` : serverPackage.identifier);898break;899case RegistryType.DOCKER:900args.push(serverPackage.version ? `${serverPackage.identifier}:${serverPackage.version}` : serverPackage.identifier);901break;902case RegistryType.NUGET:903args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);904args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here905if (serverPackage.packageArguments?.length) {906args.push('--');907}908break;909}910911if (serverPackage.packageArguments?.length) {912const result = this.processArguments(serverPackage.packageArguments);913args.push(...result.args);914inputs.push(...result.variables);915notices.push(...result.notices);916}917918return {919notices,920mcpServerConfiguration: {921config: {922type: McpServerType.LOCAL,923command: this.getCommandName(serverPackage.registryType),924args: args.length ? args : undefined,925env: Object.keys(env).length ? env : undefined,926},927inputs: inputs.length ? inputs : undefined,928}929};930}931932protected getCommandName(packageType: RegistryType): string {933switch (packageType) {934case RegistryType.NODE: return 'npx';935case RegistryType.DOCKER: return 'docker';936case RegistryType.PYTHON: return 'uvx';937case RegistryType.NUGET: return 'dnx';938}939return packageType;940}941942protected getVariables(variableInputs: Record<string, IMcpServerInput>): IMcpServerVariable[] {943const variables: IMcpServerVariable[] = [];944for (const [key, value] of Object.entries(variableInputs)) {945variables.push({946id: key,947type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,948description: value.description ?? '',949password: !!value.isSecret,950default: value.default,951options: value.choices,952});953}954return variables;955}956957private processKeyValueInputs(keyValueInputs: ReadonlyArray<IMcpServerKeyValueInput>): { inputs: Record<string, string>; variables: IMcpServerVariable[]; notices: string[] } {958const notices: string[] = [];959const inputs: Record<string, string> = {};960const variables: IMcpServerVariable[] = [];961962for (const input of keyValueInputs) {963const inputVariables = input.variables ? this.getVariables(input.variables) : [];964let value = input.value || '';965966// If explicit variables exist, use them regardless of value967if (inputVariables.length) {968for (const variable of inputVariables) {969value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);970}971variables.push(...inputVariables);972} else if (!value && (input.description || input.choices || input.default !== undefined)) {973// Only create auto-generated input variable if no explicit variables and no value974variables.push({975id: input.name,976type: input.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,977description: input.description ?? '',978password: !!input.isSecret,979default: input.default,980options: input.choices,981});982value = `\${input:${input.name}}`;983}984985inputs[input.name] = value;986}987988return { inputs, variables, notices };989}990991private processArguments(argumentsList: readonly IMcpServerArgument[]): { args: string[]; variables: IMcpServerVariable[]; notices: string[] } {992const args: string[] = [];993const variables: IMcpServerVariable[] = [];994const notices: string[] = [];995for (const arg of argumentsList) {996const argVariables = arg.variables ? this.getVariables(arg.variables) : [];997998if (arg.type === 'positional') {999let value = arg.value;1000if (value) {1001for (const variable of argVariables) {1002value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);1003}1004args.push(value);1005if (argVariables.length) {1006variables.push(...argVariables);1007}1008} else if (arg.valueHint && (arg.description || arg.default !== undefined)) {1009// Create input variable for positional argument without value1010variables.push({1011id: arg.valueHint,1012type: McpServerVariableType.PROMPT,1013description: arg.description ?? '',1014password: false,1015default: arg.default,1016});1017args.push(`\${input:${arg.valueHint}}`);1018} else {1019// Fallback to value_hint as literal1020args.push(arg.valueHint ?? '');1021}1022} else if (arg.type === 'named') {1023if (!arg.name) {1024notices.push(`Named argument is missing a name. ${JSON.stringify(arg)}`);1025continue;1026}1027args.push(arg.name);1028if (arg.value) {1029let value = arg.value;1030for (const variable of argVariables) {1031value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);1032}1033args.push(value);1034if (argVariables.length) {1035variables.push(...argVariables);1036}1037} else if (arg.description || arg.default !== undefined) {1038// Create input variable for named argument without value1039const variableId = arg.name.replace(/^--?/, '');1040variables.push({1041id: variableId,1042type: McpServerVariableType.PROMPT,1043description: arg.description ?? '',1044password: false,1045default: arg.default,1046});1047args.push(`\${input:${variableId}}`);1048}1049}1050}1051return { args, variables, notices };1052}1053}105410551056// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpPlatformTypes.ts10571058export interface IMcpDevModeConfig {1059/** Pattern or list of glob patterns to watch relative to the workspace folder. */1060watch?: string | string[];1061/** Whether to debug the MCP server when it's started. */1062debug?: { type: 'node' } | { type: 'debugpy'; debugpyPath?: string };1063}10641065export const enum McpServerVariableType {1066PROMPT = 'promptString',1067PICK = 'pickString',1068}10691070export interface IMcpServerVariable {1071readonly id: string;1072readonly type: McpServerVariableType;1073readonly description: string;1074readonly password: boolean;1075readonly default?: string;1076readonly options?: readonly string[];1077readonly serverName?: string;1078}10791080export const enum McpServerType {1081LOCAL = 'stdio',1082REMOTE = 'http',1083}10841085export interface ICommonMcpServerConfiguration {1086readonly type: McpServerType;1087readonly version?: string;1088readonly gallery?: boolean | string;1089}10901091export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfiguration {1092readonly type: McpServerType.LOCAL;1093readonly command: string;1094readonly args?: readonly string[];1095readonly env?: Record<string, string | number | null>;1096readonly envFile?: string;1097readonly cwd?: string;1098readonly dev?: IMcpDevModeConfig;1099}11001101export interface IMcpRemoteServerConfiguration extends ICommonMcpServerConfiguration {1102readonly type: McpServerType.REMOTE;1103readonly url: string;1104readonly headers?: Record<string, string>;1105readonly dev?: IMcpDevModeConfig;1106}11071108export type IMcpServerConfiguration = IMcpStdioServerConfiguration | IMcpRemoteServerConfiguration;11091110export interface IMcpServersConfiguration {1111servers?: IStringDictionary<IMcpServerConfiguration>;1112inputs?: IMcpServerVariable[];1113}11141115