Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/mcp/vscode-node/nuget.ts
13401 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 * as fs from 'fs/promises';
7
import * as os from 'os';
8
import path from 'path';
9
import { l10n } from 'vscode';
10
import { ILogService } from '../../../platform/log/common/logService';
11
import { IFetcherService } from '../../../platform/networking/common/fetcherService';
12
import { IStringDictionary } from '../../../util/vs/base/common/collections';
13
import { randomPath } from '../../../util/vs/base/common/extpath';
14
import { isObject } from '../../../util/vs/base/common/types';
15
import { ValidatePackageErrorType, ValidatePackageResult } from './commands';
16
import { CommandExecutor, ICommandExecutor } from './util';
17
18
interface NuGetServiceIndexResponse {
19
resources?: Array<{ '@id': string; '@type': string }>;
20
}
21
22
interface DotnetPackageSearchOutput {
23
searchResult?: Array<SourceResult>;
24
}
25
26
interface SourceResult {
27
sourceName: string;
28
packages?: Array<LatestPackageResult>;
29
}
30
31
interface LatestPackageResult {
32
id: string;
33
latestVersion: string;
34
owners?: string;
35
}
36
37
interface DotnetCli {
38
command: string;
39
args: Array<string>;
40
}
41
42
const MCP_SERVER_SCHEMA_2025_07_09_GH = 'https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json';
43
44
export class NuGetMcpSetup {
45
constructor(
46
public readonly logService: ILogService,
47
public readonly fetcherService: IFetcherService,
48
49
public readonly commandExecutor: ICommandExecutor = new CommandExecutor(),
50
51
public readonly dotnet: DotnetCli = { command: 'dotnet', args: [] },
52
53
// use NuGet.org central registry
54
// see https://github.com/microsoft/vscode/issues/259901 for future options
55
public readonly source: string = 'https://api.nuget.org/v3/index.json'
56
) { }
57
58
async getNuGetPackageMetadata(id: string): Promise<ValidatePackageResult> {
59
// use the home directory, which is the default for MCP servers
60
// see https://github.com/microsoft/vscode/issues/259901 for future options
61
const cwd = os.homedir();
62
63
// check for .NET CLI version for a quick "is dotnet installed?" check
64
let dotnetVersion;
65
try {
66
dotnetVersion = await this.getDotnetVersion(cwd);
67
} catch (error) {
68
const errorCode = error.hasOwnProperty('code') ? String((error as any).code) : undefined;
69
if (errorCode === 'ENOENT') {
70
return {
71
state: 'error',
72
error: l10n.t("The '{0}' command was not found. .NET SDK 10 or newer must be installed and available in PATH.", this.dotnet.command),
73
errorType: ValidatePackageErrorType.MissingCommand,
74
helpUri: 'https://aka.ms/vscode-mcp-install/dotnet',
75
helpUriLabel: l10n.t("Install .NET SDK"),
76
};
77
} else {
78
throw error;
79
}
80
}
81
82
// dnx is used for running .NET MCP servers and it was shipped with .NET 10
83
const dotnetMajorVersion = parseInt(dotnetVersion.split('.')[0]);
84
if (dotnetMajorVersion < 10) {
85
return {
86
state: 'error',
87
error: l10n.t("The installed .NET SDK must be version 10 or newer. Found {0}.", dotnetVersion),
88
errorType: ValidatePackageErrorType.BadCommandVersion,
89
helpUri: 'https://aka.ms/vscode-mcp-install/dotnet',
90
helpUriLabel: l10n.t("Update .NET SDK"),
91
};
92
}
93
94
// check if the package exists, using .NET CLI
95
const latest = await this.getLatestPackageVersion(cwd, id);
96
if (!latest) {
97
return {
98
state: 'error',
99
errorType: ValidatePackageErrorType.NotFound,
100
error: l10n.t("Package {0} does not exist on NuGet.org.", id)
101
};
102
}
103
104
// read the package readme from NuGet.org, using the HTTP API
105
const readme = await this.getPackageReadmeFromNuGetOrgAsync(latest.id, latest.version);
106
107
return {
108
state: 'ok',
109
publisher: latest.owners ?? 'unknown',
110
name: latest.id,
111
version: latest.version,
112
readme,
113
getMcpServer: async (installConsent) => {
114
// getting the server.json downloads the package, so wait for consent
115
await installConsent;
116
const manifest = await this.getServerManifest(latest.id, latest.version);
117
return mapServerJsonToMcpServer(manifest, RegistryType.NUGET);
118
},
119
};
120
}
121
122
async getServerManifest(id: string, version: string): Promise<string | undefined> {
123
this.logService.info(`Reading .mcp/server.json from NuGet package ${id}@${version}.`);
124
const installDir = randomPath(os.tmpdir(), 'vscode-nuget-mcp');
125
try {
126
// perform a local tool install using the .NET CLI
127
// this warms the cache (user packages folder) so dnx will be fast
128
// this also makes the server.json available which will be mapped to VS Code MCP config
129
await fs.mkdir(installDir, { recursive: true });
130
131
// the cwd must be the install directory or a child directory for local tool install to work
132
const cwd = installDir;
133
134
const packagesDir = await this.getGlobalPackagesPath(id, version, cwd);
135
if (!packagesDir) { return undefined; }
136
137
// explicitly create a tool manifest in the off chance one already exists in a parent directory
138
const createManifestSuccess = await this.createToolManifest(id, version, cwd);
139
if (!createManifestSuccess) { return undefined; }
140
141
const localInstallSuccess = await this.installLocalTool(id, version, cwd);
142
if (!localInstallSuccess) { return undefined; }
143
144
return await this.readServerManifest(packagesDir, id, version);
145
} catch (e) {
146
this.logService.warn(`
147
Failed to install NuGet package ${id}@${version}. Proceeding without server.json.
148
Error: ${e}`);
149
} finally {
150
try {
151
await fs.rm(installDir, { recursive: true, force: true });
152
} catch (e) {
153
this.logService.warn(`Failed to clean up temporary .NET tool install directory ${installDir}.
154
Error: ${e}`);
155
}
156
}
157
}
158
159
async getDotnetVersion(cwd: string): Promise<string> {
160
const args = this.dotnet.args.concat(['--version']);
161
const result = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);
162
const version = result.stdout.trim();
163
if (result.exitCode !== 0 || !version) {
164
this.logService.warn(`Failed to check for .NET version while checking if a NuGet MCP server exists.
165
stdout: ${result.stdout}
166
stderr: ${result.stderr}`);
167
throw new Error(`Failed to check for .NET version using '${this.dotnet.command} --version'.`);
168
}
169
170
return version;
171
}
172
173
async getLatestPackageVersion(cwd: string, id: string): Promise<{ id: string; version: string; owners?: string } | undefined> {
174
// we don't use --exact-match here because it does not return owner information on NuGet.org
175
const args = this.dotnet.args.concat(['package', 'search', id, '--source', this.source, '--prerelease', '--format', 'json']);
176
const searchResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);
177
const searchData: DotnetPackageSearchOutput = JSON.parse(searchResult.stdout.trim());
178
for (const result of searchData.searchResult ?? []) {
179
for (const pkg of result.packages ?? []) {
180
if (pkg.id.toUpperCase() === id.toUpperCase()) {
181
return { id: pkg.id, version: pkg.latestVersion, owners: pkg.owners };
182
}
183
}
184
}
185
}
186
187
async getPackageReadmeFromNuGetOrgAsync(id: string, version: string): Promise<string | undefined> {
188
try {
189
const sourceUrl = URL.parse(this.source);
190
if (sourceUrl?.protocol !== 'https:' || !sourceUrl.pathname.endsWith('.json')) {
191
this.logService.warn(`NuGet package source is not an HTTPS V3 source URL. Cannot fetch a readme for ${id}@${version}.`);
192
return;
193
}
194
195
// download the service index to locate services
196
// https://learn.microsoft.com/en-us/nuget/api/service-index
197
const serviceIndexResponse = await this.fetcherService.fetch(this.source, { method: 'GET', callSite: 'mcp-nuget-service-index' });
198
if (serviceIndexResponse.status !== 200) {
199
this.logService.warn(`Unable to read the service index for NuGet.org while fetching readme for ${id}@${version}.
200
HTTP status: ${serviceIndexResponse.status}`);
201
return;
202
}
203
204
const serviceIndex = await serviceIndexResponse.json() as NuGetServiceIndexResponse;
205
206
// try to fetch the package readme using the URL template
207
// https://learn.microsoft.com/en-us/nuget/api/readme-template-resource
208
const readmeTemplate = serviceIndex.resources?.find(resource => resource['@type'] === 'ReadmeUriTemplate/6.13.0')?.['@id'];
209
if (!readmeTemplate) {
210
this.logService.warn(`No readme URL template found for ${id}@${version} on NuGet.org.`);
211
return;
212
}
213
214
const readmeUrl = readmeTemplate
215
.replace('{lower_id}', encodeURIComponent(id.toLowerCase()))
216
.replace('{lower_version}', encodeURIComponent(version.toLowerCase()));
217
const readmeResponse = await this.fetcherService.fetch(readmeUrl, { method: 'GET', callSite: 'mcp-nuget-readme' });
218
if (readmeResponse.status === 200) {
219
return readmeResponse.text();
220
} else if (readmeResponse.status === 404) {
221
this.logService.info(`No package readme exists for ${id}@${version} on NuGet.org.`);
222
} else {
223
this.logService.warn(`Failed to read package readme for ${id}@${version} from NuGet.org.
224
HTTP status: ${readmeResponse.status}`);
225
}
226
} catch (error) {
227
this.logService.warn(`Failed to read package readme for ${id}@${version} from NuGet.org.
228
Error: ${error}`);
229
}
230
}
231
232
async getGlobalPackagesPath(id: string, version: string, cwd: string): Promise<string | undefined> {
233
const args = this.dotnet.args.concat(['nuget', 'locals', 'global-packages', '--list', '--force-english-output']);
234
const globalPackagesResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);
235
236
if (globalPackagesResult.exitCode !== 0) {
237
this.logService.warn(`Failed to discover the NuGet global packages folder. Proceeding without server.json for ${id}@${version}.
238
stdout: ${globalPackagesResult.stdout}
239
stderr: ${globalPackagesResult.stderr}`);
240
return undefined;
241
}
242
243
// output looks like:
244
// global-packages: C:\Users\username\.nuget\packages\
245
return globalPackagesResult.stdout.trim().split(' ', 2).at(-1)?.trim();
246
}
247
248
async createToolManifest(id: string, version: string, cwd: string): Promise<boolean> {
249
const args = this.dotnet.args.concat(['new', 'tool-manifest']);
250
const result = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);
251
252
if (result.exitCode !== 0) {
253
this.logService.warn(`Failed to create tool manifest.Proceeding without server.json for ${id}@${version}.
254
stdout: ${result.stdout}
255
stderr: ${result.stderr}`);
256
return false;
257
}
258
259
return true;
260
}
261
262
async installLocalTool(id: string, version: string, cwd: string): Promise<boolean> {
263
const args = this.dotnet.args.concat(['tool', 'install', `${id}@${version}`, '--source', this.source, '--local', '--create-manifest-if-needed']);
264
const installResult = await this.commandExecutor.executeWithTimeout(this.dotnet.command, args, cwd);
265
266
if (installResult.exitCode !== 0) {
267
this.logService.warn(`Failed to install local tool ${id} @${version}. Proceeding without server.json for ${id}@${version}.
268
stdout: ${installResult.stdout}
269
stderr: ${installResult.stderr}`);
270
return false;
271
}
272
273
return true;
274
}
275
276
prepareServerJson(manifest: any, id: string, version: string): any {
277
// Force the ID and version of matching NuGet package in the server.json to the one we installed.
278
// This handles cases where the server.json in the package is stale.
279
// The ID should match generally, but we'll protect against unexpected package IDs.
280
// We handle old and new schema formats:
281
// - https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json (only hosted in GitHub)
282
// - https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json (had several breaking changes over time)
283
// - https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json
284
if (manifest?.packages) {
285
for (const pkg of manifest.packages) {
286
if (!pkg) { continue; }
287
const registryType = pkg.registryType ?? pkg.registry_type ?? pkg.registry_name;
288
if (registryType === 'nuget') {
289
if (pkg.name && pkg.name !== id) {
290
this.logService.warn(`Package name mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.name}.`);
291
pkg.name = id;
292
}
293
294
if (pkg.identifier && pkg.identifier !== id) {
295
this.logService.warn(`Package identifier mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.identifier}.`);
296
pkg.identifier = id;
297
}
298
299
if (pkg.version !== version) {
300
this.logService.warn(`Package version mismatch in NuGet.mcp / server.json: expected ${version}, found ${pkg.version}.`);
301
pkg.version = version;
302
}
303
}
304
}
305
}
306
307
// the original .NET MCP server project template used a schema URL that is deprecated
308
if (manifest['$schema'] === MCP_SERVER_SCHEMA_2025_07_09_GH || !manifest['$schema']) {
309
manifest['$schema'] = McpServerSchemaVersion_v2025_07_09.SCHEMA;
310
}
311
312
// add missing properties to improve mapping
313
if (!manifest.name) { manifest.name = id; }
314
if (!manifest.description) { manifest.description = id; }
315
if (!manifest.version) { manifest.version = version; }
316
317
return manifest;
318
}
319
320
async readServerManifest(packagesDir: string, id: string, version: string): Promise<string | undefined> {
321
const serverJsonPath = path.join(packagesDir, id.toLowerCase(), version.toLowerCase(), '.mcp', 'server.json');
322
try {
323
await fs.access(serverJsonPath, fs.constants.R_OK);
324
} catch {
325
this.logService.info(`No server.json found at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
326
return undefined;
327
}
328
329
const json = await fs.readFile(serverJsonPath, 'utf8');
330
let manifest;
331
try {
332
manifest = JSON.parse(json);
333
} catch {
334
this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
335
return undefined;
336
}
337
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {
338
this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
339
return undefined;
340
}
341
342
return this.prepareServerJson(manifest, id, version);
343
}
344
}
345
346
export function mapServerJsonToMcpServer(input: unknown, registryType: RegistryType): Omit<IInstallableMcpServer, 'name'> | undefined {
347
let data: any = input;
348
349
if (!data || typeof data !== 'object' || typeof data.$schema !== 'string') {
350
return undefined;
351
}
352
353
// starting from 2025-09-29, the server.json is wrapped in a "server" property
354
if (data.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {
355
data = { server: data };
356
}
357
358
const raw = McpServerSchemaVersion_v0.SERIALIZER.toRawGalleryMcpServer(data);
359
if (!raw) {
360
return undefined;
361
}
362
363
const utility = new McpMappingUtility();
364
const result = utility.getMcpServerConfigurationFromManifest(raw, registryType);
365
return result.mcpServerConfiguration;
366
}
367
368
// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpGalleryService.ts
369
370
interface IGalleryMcpServerDataSerializer {
371
toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined;
372
}
373
374
interface IRawGalleryMcpServer {
375
readonly packages?: readonly IMcpServerPackage[];
376
readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;
377
}
378
379
export namespace McpServerSchemaVersion_v2025_07_09 {
380
381
export const VERSION = 'v0-2025-07-09';
382
export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json`;
383
384
interface RawGalleryMcpServerInput {
385
readonly description?: string;
386
readonly is_required?: boolean;
387
readonly format?: 'string' | 'number' | 'boolean' | 'filepath';
388
readonly value?: string;
389
readonly is_secret?: boolean;
390
readonly default?: string;
391
readonly choices?: readonly string[];
392
}
393
394
interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {
395
readonly variables?: Record<string, RawGalleryMcpServerInput>;
396
}
397
398
interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {
399
readonly type: 'positional';
400
readonly value_hint?: string;
401
readonly is_repeated?: boolean;
402
}
403
404
interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {
405
readonly type: 'named';
406
readonly name: string;
407
readonly is_repeated?: boolean;
408
}
409
410
interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {
411
readonly name: string;
412
readonly value?: string;
413
}
414
415
type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;
416
417
interface McpServerDeprecatedRemote {
418
readonly transport_type?: 'streamable' | 'sse';
419
readonly transport?: 'streamable' | 'sse';
420
readonly url: string;
421
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
422
}
423
424
type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport | McpServerDeprecatedRemote>;
425
426
type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;
427
428
interface StdioTransport {
429
readonly type: 'stdio';
430
}
431
432
interface StreamableHttpTransport {
433
readonly type: 'streamable-http' | 'sse';
434
readonly url: string;
435
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
436
}
437
438
interface SseTransport {
439
readonly type: 'sse';
440
readonly url: string;
441
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
442
}
443
444
interface RawGalleryMcpServerPackage {
445
readonly registry_name: string;
446
readonly name: string;
447
readonly registry_type: 'npm' | 'pypi' | 'docker-hub' | 'nuget' | 'remote' | 'mcpb';
448
readonly registry_base_url?: string;
449
readonly identifier: string;
450
readonly version: string;
451
readonly file_sha256?: string;
452
readonly transport?: RawGalleryTransport;
453
readonly package_arguments?: readonly RawGalleryMcpServerArgument[];
454
readonly runtime_hint?: string;
455
readonly runtime_arguments?: readonly RawGalleryMcpServerArgument[];
456
readonly environment_variables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
457
}
458
459
interface RawGalleryMcpServer {
460
readonly $schema: string;
461
readonly packages?: readonly RawGalleryMcpServerPackage[];
462
readonly remotes?: RawGalleryMcpServerRemotes;
463
}
464
465
class Serializer implements IGalleryMcpServerDataSerializer {
466
467
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
468
if (!input || typeof input !== 'object') {
469
return undefined;
470
}
471
472
const from = <RawGalleryMcpServer>input;
473
474
if (from.$schema && from.$schema !== McpServerSchemaVersion_v2025_07_09.SCHEMA) {
475
return undefined;
476
}
477
478
function convertServerInput(input: RawGalleryMcpServerInput): IMcpServerInput {
479
return {
480
...input,
481
isRequired: input.is_required,
482
isSecret: input.is_secret,
483
};
484
}
485
486
function convertVariables(variables: Record<string, RawGalleryMcpServerInput>): Record<string, IMcpServerInput> {
487
const result: Record<string, IMcpServerInput> = {};
488
for (const [key, value] of Object.entries(variables)) {
489
result[key] = convertServerInput(value);
490
}
491
return result;
492
}
493
494
function convertServerArgument(arg: RawGalleryMcpServerArgument): IMcpServerArgument {
495
if (arg.type === 'positional') {
496
return {
497
...arg,
498
valueHint: arg.value_hint,
499
isRepeated: arg.is_repeated,
500
isRequired: arg.is_required,
501
isSecret: arg.is_secret,
502
variables: arg.variables ? convertVariables(arg.variables) : undefined,
503
};
504
}
505
return {
506
...arg,
507
isRepeated: arg.is_repeated,
508
isRequired: arg.is_required,
509
isSecret: arg.is_secret,
510
variables: arg.variables ? convertVariables(arg.variables) : undefined,
511
};
512
}
513
514
function convertKeyValueInput(input: RawGalleryMcpServerKeyValueInput): IMcpServerKeyValueInput {
515
return {
516
...input,
517
isRequired: input.is_required,
518
isSecret: input.is_secret,
519
variables: input.variables ? convertVariables(input.variables) : undefined,
520
};
521
}
522
523
function convertTransport(input: RawGalleryTransport): Transport {
524
switch (input.type) {
525
case 'stdio':
526
return {
527
type: TransportType.STDIO,
528
};
529
case 'streamable-http':
530
return {
531
type: TransportType.STREAMABLE_HTTP,
532
url: input.url,
533
headers: input.headers?.map(convertKeyValueInput),
534
};
535
case 'sse':
536
return {
537
type: TransportType.SSE,
538
url: input.url,
539
headers: input.headers?.map(convertKeyValueInput),
540
};
541
default:
542
return {
543
type: TransportType.STDIO,
544
};
545
}
546
}
547
548
function convertRegistryType(input: string): RegistryType {
549
switch (input) {
550
case 'npm':
551
return RegistryType.NODE;
552
case 'docker':
553
case 'docker-hub':
554
case 'oci':
555
return RegistryType.DOCKER;
556
case 'pypi':
557
return RegistryType.PYTHON;
558
case 'nuget':
559
return RegistryType.NUGET;
560
case 'mcpb':
561
return RegistryType.MCPB;
562
default:
563
return RegistryType.NODE;
564
}
565
}
566
567
return {
568
packages: from.packages?.map<IMcpServerPackage>(p => ({
569
identifier: p.identifier ?? p.name,
570
registryType: convertRegistryType(p.registry_type ?? p.registry_name),
571
version: p.version,
572
fileSha256: p.file_sha256,
573
registryBaseUrl: p.registry_base_url,
574
transport: p.transport ? convertTransport(p.transport) : { type: TransportType.STDIO },
575
packageArguments: p.package_arguments?.map(convertServerArgument),
576
runtimeHint: p.runtime_hint,
577
runtimeArguments: p.runtime_arguments?.map(convertServerArgument),
578
environmentVariables: p.environment_variables?.map(convertKeyValueInput),
579
})),
580
remotes: from.remotes?.map(remote => {
581
const type = (<RawGalleryTransport>remote).type ?? (<McpServerDeprecatedRemote>remote).transport_type ?? (<McpServerDeprecatedRemote>remote).transport;
582
return {
583
type: type === TransportType.SSE ? TransportType.SSE : TransportType.STREAMABLE_HTTP,
584
url: remote.url,
585
headers: remote.headers?.map(convertKeyValueInput)
586
};
587
}),
588
};
589
}
590
}
591
592
export const SERIALIZER = new Serializer();
593
}
594
595
namespace McpServerSchemaVersion_v0_1 {
596
597
export const VERSION = 'v0.1';
598
export const SCHEMA = `https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json`;
599
600
interface RawGalleryMcpServerInput {
601
readonly choices?: readonly string[];
602
readonly default?: string;
603
readonly description?: string;
604
readonly format?: 'string' | 'number' | 'boolean' | 'filepath';
605
readonly isRequired?: boolean;
606
readonly isSecret?: boolean;
607
readonly placeholder?: string;
608
readonly value?: string;
609
}
610
611
interface RawGalleryMcpServerVariableInput extends RawGalleryMcpServerInput {
612
readonly variables?: Record<string, RawGalleryMcpServerInput>;
613
}
614
615
interface RawGalleryMcpServerPositionalArgument extends RawGalleryMcpServerVariableInput {
616
readonly type: 'positional';
617
readonly valueHint?: string;
618
readonly isRepeated?: boolean;
619
}
620
621
interface RawGalleryMcpServerNamedArgument extends RawGalleryMcpServerVariableInput {
622
readonly type: 'named';
623
readonly name: string;
624
readonly isRepeated?: boolean;
625
}
626
627
interface RawGalleryMcpServerKeyValueInput extends RawGalleryMcpServerVariableInput {
628
readonly name: string;
629
}
630
631
type RawGalleryMcpServerArgument = RawGalleryMcpServerPositionalArgument | RawGalleryMcpServerNamedArgument;
632
633
type RawGalleryMcpServerRemotes = ReadonlyArray<SseTransport | StreamableHttpTransport>;
634
635
type RawGalleryTransport = StdioTransport | StreamableHttpTransport | SseTransport;
636
637
interface StdioTransport {
638
readonly type: TransportType.STDIO;
639
}
640
641
interface StreamableHttpTransport {
642
readonly type: TransportType.STREAMABLE_HTTP;
643
readonly url: string;
644
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
645
}
646
647
interface SseTransport {
648
readonly type: TransportType.SSE;
649
readonly url: string;
650
readonly headers?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
651
}
652
653
interface RawGalleryMcpServerPackage {
654
readonly registryType: RegistryType;
655
readonly identifier: string;
656
readonly version: string;
657
readonly transport: RawGalleryTransport;
658
readonly registryBaseUrl?: string;
659
readonly fileSha256?: string;
660
readonly packageArguments?: readonly RawGalleryMcpServerArgument[];
661
readonly runtimeHint?: string;
662
readonly runtimeArguments?: readonly RawGalleryMcpServerArgument[];
663
readonly environmentVariables?: ReadonlyArray<RawGalleryMcpServerKeyValueInput>;
664
}
665
666
interface RawGalleryMcpServer {
667
readonly $schema: string;
668
readonly packages?: readonly RawGalleryMcpServerPackage[];
669
readonly remotes?: RawGalleryMcpServerRemotes;
670
}
671
672
interface RawGalleryMcpServerInfo {
673
readonly server: RawGalleryMcpServer;
674
}
675
676
class Serializer implements IGalleryMcpServerDataSerializer {
677
678
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
679
if (!input || typeof input !== 'object') {
680
return undefined;
681
}
682
683
const from = <RawGalleryMcpServerInfo>input;
684
685
if (
686
(!from.server || !isObject(from.server))
687
) {
688
return undefined;
689
}
690
691
if (from.server.$schema && from.server.$schema !== McpServerSchemaVersion_v0_1.SCHEMA) {
692
return undefined;
693
}
694
695
return {
696
packages: from.server.packages,
697
remotes: from.server.remotes,
698
};
699
}
700
}
701
702
export const SERIALIZER = new Serializer();
703
}
704
705
export namespace McpServerSchemaVersion_v0 {
706
707
export const VERSION = 'v0';
708
709
class Serializer implements IGalleryMcpServerDataSerializer {
710
711
private readonly galleryMcpServerDataSerializers: IGalleryMcpServerDataSerializer[] = [];
712
713
constructor() {
714
this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v0_1.SERIALIZER);
715
this.galleryMcpServerDataSerializers.push(McpServerSchemaVersion_v2025_07_09.SERIALIZER);
716
}
717
718
public toRawGalleryMcpServer(input: unknown): IRawGalleryMcpServer | undefined {
719
for (const serializer of this.galleryMcpServerDataSerializers) {
720
const result = serializer.toRawGalleryMcpServer(input);
721
if (result) {
722
return result;
723
}
724
}
725
return undefined;
726
}
727
}
728
729
export const SERIALIZER = new Serializer();
730
}
731
732
733
export interface IMcpServerInput {
734
readonly description?: string;
735
readonly isRequired?: boolean;
736
readonly format?: 'string' | 'number' | 'boolean' | 'filepath';
737
readonly value?: string;
738
readonly isSecret?: boolean;
739
readonly default?: string;
740
readonly choices?: readonly string[];
741
}
742
743
export interface IMcpServerVariableInput extends IMcpServerInput {
744
readonly variables?: Record<string, IMcpServerInput>;
745
}
746
747
export interface IMcpServerPositionalArgument extends IMcpServerVariableInput {
748
readonly type: 'positional';
749
readonly valueHint?: string;
750
readonly isRepeated?: boolean;
751
}
752
753
export interface IMcpServerNamedArgument extends IMcpServerVariableInput {
754
readonly type: 'named';
755
readonly name: string;
756
readonly isRepeated?: boolean;
757
}
758
759
export interface IMcpServerKeyValueInput extends IMcpServerVariableInput {
760
readonly name: string;
761
readonly value?: string;
762
}
763
764
export type IMcpServerArgument = IMcpServerPositionalArgument | IMcpServerNamedArgument;
765
766
export const enum RegistryType {
767
NODE = 'npm',
768
PYTHON = 'pypi',
769
DOCKER = 'oci',
770
NUGET = 'nuget',
771
MCPB = 'mcpb',
772
REMOTE = 'remote'
773
}
774
775
export const enum TransportType {
776
STDIO = 'stdio',
777
STREAMABLE_HTTP = 'streamable-http',
778
SSE = 'sse'
779
}
780
781
export interface StdioTransport {
782
readonly type: TransportType.STDIO;
783
}
784
785
export interface StreamableHttpTransport {
786
readonly type: TransportType.STREAMABLE_HTTP;
787
readonly url: string;
788
readonly headers?: ReadonlyArray<IMcpServerKeyValueInput>;
789
}
790
791
export interface SseTransport {
792
readonly type: TransportType.SSE;
793
readonly url: string;
794
readonly headers?: ReadonlyArray<IMcpServerKeyValueInput>;
795
}
796
797
export type Transport = StdioTransport | StreamableHttpTransport | SseTransport;
798
799
export interface IMcpServerPackage {
800
readonly registryType: RegistryType;
801
readonly identifier: string;
802
readonly version: string;
803
readonly transport?: Transport;
804
readonly registryBaseUrl?: string;
805
readonly fileSha256?: string;
806
readonly packageArguments?: readonly IMcpServerArgument[];
807
readonly runtimeHint?: string;
808
readonly runtimeArguments?: readonly IMcpServerArgument[];
809
readonly environmentVariables?: ReadonlyArray<IMcpServerKeyValueInput>;
810
}
811
812
export interface IGalleryMcpServerConfiguration {
813
readonly packages?: readonly IMcpServerPackage[];
814
readonly remotes?: ReadonlyArray<SseTransport | StreamableHttpTransport>;
815
}
816
817
export const enum GalleryMcpServerStatus {
818
Active = 'active',
819
Deprecated = 'deprecated'
820
}
821
822
export interface IInstallableMcpServer {
823
readonly name: string;
824
readonly config: IMcpServerConfiguration;
825
readonly inputs?: IMcpServerVariable[];
826
}
827
828
export type McpServerConfiguration = Omit<IInstallableMcpServer, 'name'>;
829
export interface McpServerConfigurationParseResult {
830
readonly mcpServerConfiguration: McpServerConfiguration;
831
readonly notices: string[];
832
}
833
834
835
// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpManagementService.ts
836
837
export class McpMappingUtility {
838
getMcpServerConfigurationFromManifest(manifest: IGalleryMcpServerConfiguration, packageType: RegistryType): McpServerConfigurationParseResult {
839
840
// remote
841
if (packageType === RegistryType.REMOTE && manifest.remotes?.length) {
842
const { inputs, variables } = this.processKeyValueInputs(manifest.remotes[0].headers ?? []);
843
return {
844
mcpServerConfiguration: {
845
config: {
846
type: McpServerType.REMOTE,
847
url: manifest.remotes[0].url,
848
headers: Object.keys(inputs).length ? inputs : undefined,
849
},
850
inputs: variables.length ? variables : undefined,
851
},
852
notices: [],
853
};
854
}
855
856
// local
857
const serverPackage = manifest.packages?.find(p => p.registryType === packageType) ?? manifest.packages?.[0];
858
if (!serverPackage) {
859
throw new Error(`No server package found`);
860
}
861
862
const args: string[] = [];
863
const inputs: IMcpServerVariable[] = [];
864
const env: Record<string, string> = {};
865
const notices: string[] = [];
866
867
if (serverPackage.registryType === RegistryType.DOCKER) {
868
args.push('run');
869
args.push('-i');
870
args.push('--rm');
871
}
872
873
if (serverPackage.runtimeArguments?.length) {
874
const result = this.processArguments(serverPackage.runtimeArguments ?? []);
875
args.push(...result.args);
876
inputs.push(...result.variables);
877
notices.push(...result.notices);
878
}
879
880
if (serverPackage.environmentVariables?.length) {
881
const { inputs: envInputs, variables: envVariables, notices: envNotices } = this.processKeyValueInputs(serverPackage.environmentVariables ?? []);
882
inputs.push(...envVariables);
883
notices.push(...envNotices);
884
for (const [name, value] of Object.entries(envInputs)) {
885
env[name] = value;
886
if (serverPackage.registryType === RegistryType.DOCKER) {
887
args.push('-e');
888
args.push(name);
889
}
890
}
891
}
892
893
switch (serverPackage.registryType) {
894
case RegistryType.NODE:
895
args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);
896
break;
897
case RegistryType.PYTHON:
898
args.push(serverPackage.version ? `${serverPackage.identifier}==${serverPackage.version}` : serverPackage.identifier);
899
break;
900
case RegistryType.DOCKER:
901
args.push(serverPackage.version ? `${serverPackage.identifier}:${serverPackage.version}` : serverPackage.identifier);
902
break;
903
case RegistryType.NUGET:
904
args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);
905
args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here
906
if (serverPackage.packageArguments?.length) {
907
args.push('--');
908
}
909
break;
910
}
911
912
if (serverPackage.packageArguments?.length) {
913
const result = this.processArguments(serverPackage.packageArguments);
914
args.push(...result.args);
915
inputs.push(...result.variables);
916
notices.push(...result.notices);
917
}
918
919
return {
920
notices,
921
mcpServerConfiguration: {
922
config: {
923
type: McpServerType.LOCAL,
924
command: this.getCommandName(serverPackage.registryType),
925
args: args.length ? args : undefined,
926
env: Object.keys(env).length ? env : undefined,
927
},
928
inputs: inputs.length ? inputs : undefined,
929
}
930
};
931
}
932
933
protected getCommandName(packageType: RegistryType): string {
934
switch (packageType) {
935
case RegistryType.NODE: return 'npx';
936
case RegistryType.DOCKER: return 'docker';
937
case RegistryType.PYTHON: return 'uvx';
938
case RegistryType.NUGET: return 'dnx';
939
}
940
return packageType;
941
}
942
943
protected getVariables(variableInputs: Record<string, IMcpServerInput>): IMcpServerVariable[] {
944
const variables: IMcpServerVariable[] = [];
945
for (const [key, value] of Object.entries(variableInputs)) {
946
variables.push({
947
id: key,
948
type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
949
description: value.description ?? '',
950
password: !!value.isSecret,
951
default: value.default,
952
options: value.choices,
953
});
954
}
955
return variables;
956
}
957
958
private processKeyValueInputs(keyValueInputs: ReadonlyArray<IMcpServerKeyValueInput>): { inputs: Record<string, string>; variables: IMcpServerVariable[]; notices: string[] } {
959
const notices: string[] = [];
960
const inputs: Record<string, string> = {};
961
const variables: IMcpServerVariable[] = [];
962
963
for (const input of keyValueInputs) {
964
const inputVariables = input.variables ? this.getVariables(input.variables) : [];
965
let value = input.value || '';
966
967
// If explicit variables exist, use them regardless of value
968
if (inputVariables.length) {
969
for (const variable of inputVariables) {
970
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
971
}
972
variables.push(...inputVariables);
973
} else if (!value && (input.description || input.choices || input.default !== undefined)) {
974
// Only create auto-generated input variable if no explicit variables and no value
975
variables.push({
976
id: input.name,
977
type: input.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
978
description: input.description ?? '',
979
password: !!input.isSecret,
980
default: input.default,
981
options: input.choices,
982
});
983
value = `\${input:${input.name}}`;
984
}
985
986
inputs[input.name] = value;
987
}
988
989
return { inputs, variables, notices };
990
}
991
992
private processArguments(argumentsList: readonly IMcpServerArgument[]): { args: string[]; variables: IMcpServerVariable[]; notices: string[] } {
993
const args: string[] = [];
994
const variables: IMcpServerVariable[] = [];
995
const notices: string[] = [];
996
for (const arg of argumentsList) {
997
const argVariables = arg.variables ? this.getVariables(arg.variables) : [];
998
999
if (arg.type === 'positional') {
1000
let value = arg.value;
1001
if (value) {
1002
for (const variable of argVariables) {
1003
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
1004
}
1005
args.push(value);
1006
if (argVariables.length) {
1007
variables.push(...argVariables);
1008
}
1009
} else if (arg.valueHint && (arg.description || arg.default !== undefined)) {
1010
// Create input variable for positional argument without value
1011
variables.push({
1012
id: arg.valueHint,
1013
type: McpServerVariableType.PROMPT,
1014
description: arg.description ?? '',
1015
password: false,
1016
default: arg.default,
1017
});
1018
args.push(`\${input:${arg.valueHint}}`);
1019
} else {
1020
// Fallback to value_hint as literal
1021
args.push(arg.valueHint ?? '');
1022
}
1023
} else if (arg.type === 'named') {
1024
if (!arg.name) {
1025
notices.push(`Named argument is missing a name. ${JSON.stringify(arg)}`);
1026
continue;
1027
}
1028
args.push(arg.name);
1029
if (arg.value) {
1030
let value = arg.value;
1031
for (const variable of argVariables) {
1032
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
1033
}
1034
args.push(value);
1035
if (argVariables.length) {
1036
variables.push(...argVariables);
1037
}
1038
} else if (arg.description || arg.default !== undefined) {
1039
// Create input variable for named argument without value
1040
const variableId = arg.name.replace(/^--?/, '');
1041
variables.push({
1042
id: variableId,
1043
type: McpServerVariableType.PROMPT,
1044
description: arg.description ?? '',
1045
password: false,
1046
default: arg.default,
1047
});
1048
args.push(`\${input:${variableId}}`);
1049
}
1050
}
1051
}
1052
return { args, variables, notices };
1053
}
1054
}
1055
1056
1057
// Copied from https://github.com/microsoft/vscode/blob/f8e2f71c2f78ac1ce63389e761e2aefc724646fc/src/vs/platform/mcp/common/mcpPlatformTypes.ts
1058
1059
export interface IMcpDevModeConfig {
1060
/** Pattern or list of glob patterns to watch relative to the workspace folder. */
1061
watch?: string | string[];
1062
/** Whether to debug the MCP server when it's started. */
1063
debug?: { type: 'node' } | { type: 'debugpy'; debugpyPath?: string };
1064
}
1065
1066
export const enum McpServerVariableType {
1067
PROMPT = 'promptString',
1068
PICK = 'pickString',
1069
}
1070
1071
export interface IMcpServerVariable {
1072
readonly id: string;
1073
readonly type: McpServerVariableType;
1074
readonly description: string;
1075
readonly password: boolean;
1076
readonly default?: string;
1077
readonly options?: readonly string[];
1078
readonly serverName?: string;
1079
}
1080
1081
export const enum McpServerType {
1082
LOCAL = 'stdio',
1083
REMOTE = 'http',
1084
}
1085
1086
export interface ICommonMcpServerConfiguration {
1087
readonly type: McpServerType;
1088
readonly version?: string;
1089
readonly gallery?: boolean | string;
1090
}
1091
1092
export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfiguration {
1093
readonly type: McpServerType.LOCAL;
1094
readonly command: string;
1095
readonly args?: readonly string[];
1096
readonly env?: Record<string, string | number | null>;
1097
readonly envFile?: string;
1098
readonly cwd?: string;
1099
readonly dev?: IMcpDevModeConfig;
1100
}
1101
1102
export interface IMcpRemoteServerConfiguration extends ICommonMcpServerConfiguration {
1103
readonly type: McpServerType.REMOTE;
1104
readonly url: string;
1105
readonly headers?: Record<string, string>;
1106
readonly dev?: IMcpDevModeConfig;
1107
}
1108
1109
export type IMcpServerConfiguration = IMcpStdioServerConfiguration | IMcpRemoteServerConfiguration;
1110
1111
export interface IMcpServersConfiguration {
1112
servers?: IStringDictionary<IMcpServerConfiguration>;
1113
inputs?: IMcpServerVariable[];
1114
}
1115