Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts
5250 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 { mapFindFirst } from '../../../../base/common/arraysFind.js';
7
import { assertNever } from '../../../../base/common/assert.js';
8
import { disposableTimeout } from '../../../../base/common/async.js';
9
import { parse as parseJsonc } from '../../../../base/common/jsonc.js';
10
import { DisposableStore } from '../../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../../base/common/network.js';
12
import { autorun } from '../../../../base/common/observable.js';
13
import { basename } from '../../../../base/common/resources.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { generateUuid } from '../../../../base/common/uuid.js';
16
import { localize } from '../../../../nls.js';
17
import { ICommandService } from '../../../../platform/commands/common/commands.js';
18
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { ILabelService } from '../../../../platform/label/common/label.js';
21
import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
22
import { IGalleryMcpServerConfiguration, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js';
23
import { INotificationService } from '../../../../platform/notification/common/notification.js';
24
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
25
import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js';
26
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
27
import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
28
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
29
import { IEditorService } from '../../../services/editor/common/editorService.js';
30
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
31
import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
32
import { McpCommandIds } from '../common/mcpCommandIds.js';
33
import { allDiscoverySources, DiscoverySource, mcpDiscoverySection, mcpStdioServerSchema } from '../common/mcpConfiguration.js';
34
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
35
import { IMcpService, McpConnectionState } from '../common/mcpTypes.js';
36
import { ILogService } from '../../../../platform/log/common/log.js';
37
38
export const enum AddConfigurationType {
39
Stdio,
40
HTTP,
41
42
NpmPackage,
43
PipPackage,
44
NuGetPackage,
45
DockerImage,
46
}
47
48
type AssistedConfigurationType = AddConfigurationType.NpmPackage | AddConfigurationType.PipPackage | AddConfigurationType.NuGetPackage | AddConfigurationType.DockerImage;
49
50
export const AssistedTypes = {
51
[AddConfigurationType.NpmPackage]: {
52
title: localize('mcp.npm.title', "Enter NPM Package Name"),
53
placeholder: localize('mcp.npm.placeholder', "Package name (e.g., @org/package)"),
54
pickLabel: localize('mcp.serverType.npm', "NPM Package"),
55
pickDescription: localize('mcp.serverType.npm.description', "Install from an NPM package name"),
56
enabledConfigKey: null, // always enabled
57
},
58
[AddConfigurationType.PipPackage]: {
59
title: localize('mcp.pip.title', "Enter Pip Package Name"),
60
placeholder: localize('mcp.pip.placeholder', "Package name (e.g., package-name)"),
61
pickLabel: localize('mcp.serverType.pip', "Pip Package"),
62
pickDescription: localize('mcp.serverType.pip.description', "Install from a Pip package name"),
63
enabledConfigKey: null, // always enabled
64
},
65
[AddConfigurationType.NuGetPackage]: {
66
title: localize('mcp.nuget.title', "Enter NuGet Package Name"),
67
placeholder: localize('mcp.nuget.placeholder', "Package name (e.g., Package.Name)"),
68
pickLabel: localize('mcp.serverType.nuget', "NuGet Package"),
69
pickDescription: localize('mcp.serverType.nuget.description', "Install from a NuGet package name"),
70
enabledConfigKey: 'chat.mcp.assisted.nuget.enabled',
71
},
72
[AddConfigurationType.DockerImage]: {
73
title: localize('mcp.docker.title', "Enter Docker Image Name"),
74
placeholder: localize('mcp.docker.placeholder', "Image name (e.g., mcp/imagename)"),
75
pickLabel: localize('mcp.serverType.docker', "Docker Image"),
76
pickDescription: localize('mcp.serverType.docker.description', "Install from a Docker image"),
77
enabledConfigKey: null, // always enabled
78
},
79
};
80
81
const enum AddConfigurationCopilotCommand {
82
/** Returns whether MCP enhanced setup is enabled. */
83
IsSupported = 'github.copilot.chat.mcp.setup.check',
84
85
/** Takes an npm/pip package name, validates its owner. */
86
ValidatePackage = 'github.copilot.chat.mcp.setup.validatePackage',
87
88
/** Returns the resolved MCP configuration. */
89
StartFlow = 'github.copilot.chat.mcp.setup.flow',
90
}
91
92
type ValidatePackageResult =
93
{ state: 'ok'; publisher: string; name?: string; version?: string }
94
| { state: 'error'; error: string; helpUri?: string; helpUriLabel?: string };
95
96
type AddServerData = {
97
packageType: string;
98
};
99
type AddServerClassification = {
100
owner: 'digitarald';
101
comment: 'Generic details for adding a new MCP server';
102
packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' };
103
};
104
type AddServerCompletedData = {
105
packageType: string;
106
serverType: string | undefined;
107
target: string;
108
};
109
type AddServerCompletedClassification = {
110
owner: 'digitarald';
111
comment: 'Generic details for successfully adding model-assisted MCP server';
112
packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' };
113
serverType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server' };
114
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The target of the MCP server configuration' };
115
};
116
117
type AssistedServerConfiguration = {
118
type?: 'assisted';
119
name?: string;
120
server: Omit<IMcpStdioServerConfiguration, 'type'>;
121
inputs?: IMcpServerVariable[];
122
inputValues?: Record<string, string>;
123
} | {
124
type: 'mapped';
125
name?: string;
126
server: Omit<IMcpStdioServerConfiguration, 'type'>;
127
inputs?: IMcpServerVariable[];
128
};
129
130
export class McpAddConfigurationCommand {
131
constructor(
132
private readonly workspaceFolder: IWorkspaceFolder | undefined,
133
@IQuickInputService private readonly _quickInputService: IQuickInputService,
134
@IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService,
135
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
136
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
137
@ICommandService private readonly _commandService: ICommandService,
138
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
139
@IOpenerService private readonly _openerService: IOpenerService,
140
@IEditorService private readonly _editorService: IEditorService,
141
@IFileService private readonly _fileService: IFileService,
142
@INotificationService private readonly _notificationService: INotificationService,
143
@ITelemetryService private readonly _telemetryService: ITelemetryService,
144
@IMcpService private readonly _mcpService: IMcpService,
145
@ILabelService private readonly _label: ILabelService,
146
@IConfigurationService private readonly _configurationService: IConfigurationService,
147
) { }
148
149
private async getServerType(): Promise<AddConfigurationType | undefined> {
150
type TItem = { kind: AddConfigurationType | 'browse' | 'discovery' } & IQuickPickItem;
151
const items: QuickPickInput<TItem>[] = [
152
{ kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") },
153
{ kind: AddConfigurationType.HTTP, label: localize('mcp.serverType.http', "HTTP (HTTP or Server-Sent Events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") }
154
];
155
156
let aiSupported: boolean | undefined;
157
try {
158
aiSupported = await this._commandService.executeCommand<boolean>(AddConfigurationCopilotCommand.IsSupported);
159
} catch {
160
// ignored
161
}
162
163
if (aiSupported) {
164
items.unshift({ type: 'separator', label: localize('mcp.serverType.manual', "Manual Install") });
165
166
const elligableTypes = Object.entries(AssistedTypes).map(([type, { pickLabel, pickDescription, enabledConfigKey }]) => {
167
if (enabledConfigKey) {
168
const enabled = this._configurationService.getValue<boolean>(enabledConfigKey) ?? false;
169
if (!enabled) {
170
return;
171
}
172
}
173
return {
174
kind: Number(type) as AddConfigurationType,
175
label: pickLabel,
176
description: pickDescription,
177
};
178
}).filter(x => !!x);
179
180
items.push(
181
{ type: 'separator', label: localize('mcp.serverType.copilot', "Model-Assisted") },
182
...elligableTypes
183
);
184
}
185
186
items.push({ type: 'separator' });
187
188
const discovery = this._configurationService.getValue<{ [K in DiscoverySource]: boolean }>(mcpDiscoverySection);
189
if (discovery && typeof discovery === 'object' && allDiscoverySources.some(d => !discovery[d])) {
190
items.push({
191
kind: 'discovery',
192
label: localize('mcp.servers.discovery', "Add from another application..."),
193
});
194
}
195
196
items.push({
197
kind: 'browse',
198
label: localize('mcp.servers.browse', "Browse MCP Servers..."),
199
});
200
201
const result = await this._quickInputService.pick<TItem>(items, {
202
placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add"),
203
});
204
205
if (result?.kind === 'browse') {
206
this._commandService.executeCommand(McpCommandIds.Browse);
207
return undefined;
208
}
209
210
if (result?.kind === 'discovery') {
211
this._commandService.executeCommand('workbench.action.openSettings', mcpDiscoverySection);
212
return undefined;
213
}
214
215
return result?.kind;
216
}
217
218
private async getStdioConfig(): Promise<IMcpStdioServerConfiguration | undefined> {
219
const command = await this._quickInputService.input({
220
title: localize('mcp.command.title', "Enter Command"),
221
placeHolder: localize('mcp.command.placeholder', "Command to run (with optional arguments)"),
222
ignoreFocusLost: true,
223
});
224
225
if (!command) {
226
return undefined;
227
}
228
229
this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {
230
packageType: 'stdio'
231
});
232
233
// Split command into command and args, handling quotes
234
const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g)!;
235
return {
236
type: McpServerType.LOCAL,
237
command: parts[0].replace(/"/g, ''),
238
239
args: parts.slice(1).map(arg => arg.replace(/"/g, ''))
240
};
241
}
242
243
private async getSSEConfig(): Promise<IMcpRemoteServerConfiguration | undefined> {
244
const url = await this._quickInputService.input({
245
title: localize('mcp.url.title', "Enter Server URL"),
246
placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"),
247
ignoreFocusLost: true,
248
});
249
250
if (!url) {
251
return undefined;
252
}
253
254
this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {
255
packageType: 'sse'
256
});
257
258
return { url, type: McpServerType.REMOTE };
259
}
260
261
private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise<string | undefined> {
262
const id = await this._quickInputService.input({
263
title: localize('mcp.serverId.title', "Enter Server ID"),
264
placeHolder: localize('mcp.serverId.placeholder', "Unique identifier for this server"),
265
value: suggestion,
266
ignoreFocusLost: true,
267
});
268
269
return id;
270
}
271
272
private async getConfigurationTarget(): Promise<ConfigurationTarget | IWorkspaceFolder | undefined> {
273
const options: (IQuickPickItem & { target?: ConfigurationTarget | IWorkspaceFolder })[] = [
274
{ target: ConfigurationTarget.USER_LOCAL, label: localize('mcp.target.user', "Global"), description: localize('mcp.target.user.description', "Available in all workspaces, runs locally") }
275
];
276
277
const raLabel = this._environmentService.remoteAuthority && this._label.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);
278
if (raLabel) {
279
options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote"), description: localize('mcp.target..remote.description', "Available on this remote machine, runs on {0}", raLabel) });
280
}
281
282
const workbenchState = this._workspaceService.getWorkbenchState();
283
if (workbenchState !== WorkbenchState.EMPTY) {
284
const target = workbenchState === WorkbenchState.FOLDER ? this._workspaceService.getWorkspace().folders[0] : ConfigurationTarget.WORKSPACE;
285
if (this._environmentService.remoteAuthority) {
286
options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description.remote', "Available in this workspace, runs on {0}", raLabel) });
287
} else {
288
options.push({ target, label: localize('mcp.target.workspace', "Workspace"), description: localize('mcp.target.workspace.description', "Available in this workspace, runs locally") });
289
}
290
}
291
292
if (options.length === 1) {
293
return options[0].target;
294
}
295
296
const targetPick = await this._quickInputService.pick(options, {
297
title: localize('mcp.target.title', "Add MCP Server"),
298
placeHolder: localize('mcp.target.placeholder', "Select the configuration target")
299
});
300
301
return targetPick?.target;
302
}
303
304
private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name?: string; server: Omit<IMcpStdioServerConfiguration, 'type'>; inputs?: IMcpServerVariable[]; inputValues?: Record<string, string> } | undefined> {
305
const packageName = await this._quickInputService.input({
306
ignoreFocusLost: true,
307
title: AssistedTypes[type].title,
308
placeHolder: AssistedTypes[type].placeholder,
309
});
310
311
if (!packageName) {
312
return undefined;
313
}
314
315
const enum LoadAction {
316
Retry = 'retry',
317
Cancel = 'cancel',
318
Allow = 'allow',
319
OpenUri = 'openUri',
320
}
321
322
const loadingQuickPickStore = new DisposableStore();
323
const loadingQuickPick = loadingQuickPickStore.add(this._quickInputService.createQuickPick<IQuickPickItem & { id: LoadAction; helpUri?: URI }>());
324
loadingQuickPick.title = localize('mcp.loading.title', "Loading package details...");
325
loadingQuickPick.busy = true;
326
loadingQuickPick.ignoreFocusOut = true;
327
328
const packageType = this.getPackageType(type);
329
330
this._telemetryService.publicLog2<AddServerData, AddServerClassification>('mcp.addserver', {
331
packageType: packageType!
332
});
333
334
this._commandService.executeCommand<ValidatePackageResult>(
335
AddConfigurationCopilotCommand.ValidatePackage,
336
{
337
type: packageType,
338
name: packageName,
339
targetConfig: {
340
...mcpStdioServerSchema,
341
properties: {
342
...mcpStdioServerSchema.properties,
343
name: {
344
type: 'string',
345
description: 'Suggested name of the server, alphanumeric and hyphen only',
346
}
347
},
348
required: [...(mcpStdioServerSchema.required || []), 'name'],
349
},
350
}
351
).then(result => {
352
if (!result || result.state === 'error') {
353
loadingQuickPick.title = result?.error || 'Unknown error loading package';
354
355
const items: Array<IQuickPickItem & { id: LoadAction; helpUri?: URI }> = [];
356
357
if (result?.helpUri) {
358
items.push({
359
id: LoadAction.OpenUri,
360
label: result.helpUriLabel ?? localize('mcp.error.openHelpUri', 'Open help URL'),
361
helpUri: URI.parse(result.helpUri),
362
});
363
}
364
365
items.push(
366
{ id: LoadAction.Retry, label: localize('mcp.error.retry', 'Try a different package') },
367
{ id: LoadAction.Cancel, label: localize('cancel', 'Cancel') },
368
);
369
370
loadingQuickPick.items = items;
371
} else {
372
loadingQuickPick.title = localize(
373
'mcp.confirmPublish', 'Install {0}{1} from {2}?',
374
result.name ?? packageName,
375
result.version ? `@${result.version}` : '',
376
result.publisher);
377
loadingQuickPick.items = [
378
{ id: LoadAction.Allow, label: localize('allow', "Allow") },
379
{ id: LoadAction.Cancel, label: localize('cancel', 'Cancel') }
380
];
381
}
382
loadingQuickPick.busy = false;
383
});
384
385
const loadingAction = await new Promise<{ id: LoadAction; helpUri?: URI } | undefined>(resolve => {
386
loadingQuickPickStore.add(loadingQuickPick.onDidAccept(() => resolve(loadingQuickPick.selectedItems[0])));
387
loadingQuickPickStore.add(loadingQuickPick.onDidHide(() => resolve(undefined)));
388
loadingQuickPick.show();
389
}).finally(() => loadingQuickPickStore.dispose());
390
391
switch (loadingAction?.id) {
392
case LoadAction.Retry:
393
return this.getAssistedConfig(type);
394
case LoadAction.OpenUri:
395
if (loadingAction.helpUri) { this._openerService.open(loadingAction.helpUri); }
396
return undefined;
397
case LoadAction.Allow:
398
break;
399
case LoadAction.Cancel:
400
default:
401
return undefined;
402
}
403
404
const config = await this._commandService.executeCommand<AssistedServerConfiguration>(
405
AddConfigurationCopilotCommand.StartFlow,
406
{
407
name: packageName,
408
type: packageType
409
}
410
);
411
412
if (config?.type === 'mapped') {
413
return {
414
name: config.name,
415
server: config.server,
416
inputs: config.inputs,
417
};
418
} else if (config?.type === 'assisted' || !config?.type) {
419
return config;
420
} else {
421
assertNever(config?.type);
422
}
423
}
424
425
/** Shows the location of a server config once it's discovered. */
426
private showOnceDiscovered(name: string) {
427
const store = new DisposableStore();
428
store.add(autorun(reader => {
429
const colls = this._mcpRegistry.collections.read(reader);
430
const servers = this._mcpService.servers.read(reader);
431
const match = mapFindFirst(colls, collection => mapFindFirst(collection.serverDefinitions.read(reader),
432
server => server.label === name ? { server, collection } : undefined));
433
const server = match && servers.find(s => s.definition.id === match.server.id);
434
435
436
if (match && server) {
437
if (match.collection.presentation?.origin) {
438
this._editorService.openEditor({
439
resource: match.collection.presentation.origin,
440
options: {
441
selection: match.server.presentation?.origin?.range,
442
preserveFocus: true,
443
}
444
});
445
} else {
446
this._commandService.executeCommand(McpCommandIds.ServerOptions, name);
447
}
448
449
server.start({ promptType: 'all-untrusted' }).then(state => {
450
if (state.state === McpConnectionState.Kind.Error) {
451
server.showOutput();
452
}
453
});
454
455
store.dispose();
456
}
457
}));
458
459
store.add(disposableTimeout(() => store.dispose(), 5000));
460
}
461
462
public async run(): Promise<void> {
463
// Step 1: Choose server type
464
const serverType = await this.getServerType();
465
if (serverType === undefined) {
466
return;
467
}
468
469
// Step 2: Get server details based on type
470
let config: IMcpServerConfiguration | undefined;
471
let suggestedName: string | undefined;
472
let inputs: IMcpServerVariable[] | undefined;
473
let inputValues: Record<string, string> | undefined;
474
switch (serverType) {
475
case AddConfigurationType.Stdio:
476
config = await this.getStdioConfig();
477
break;
478
case AddConfigurationType.HTTP:
479
config = await this.getSSEConfig();
480
break;
481
case AddConfigurationType.NpmPackage:
482
case AddConfigurationType.PipPackage:
483
case AddConfigurationType.NuGetPackage:
484
case AddConfigurationType.DockerImage: {
485
const r = await this.getAssistedConfig(serverType);
486
config = r?.server ? { ...r.server, type: McpServerType.LOCAL } : undefined;
487
suggestedName = r?.name;
488
inputs = r?.inputs;
489
inputValues = r?.inputValues;
490
break;
491
}
492
default:
493
assertNever(serverType);
494
}
495
496
if (!config) {
497
return;
498
}
499
500
// Step 3: Get server ID
501
const name = await this.getServerId(suggestedName);
502
if (!name) {
503
return;
504
}
505
506
// Step 4: Choose configuration target if no configUri provided
507
let target: ConfigurationTarget | IWorkspaceFolder | undefined = this.workspaceFolder;
508
if (!target) {
509
target = await this.getConfigurationTarget();
510
if (!target) {
511
return;
512
}
513
}
514
515
await this._mcpManagementService.install({ name, config, inputs }, { target });
516
517
if (inputValues) {
518
for (const [key, value] of Object.entries(inputValues)) {
519
await this._mcpRegistry.setSavedInput(key, (isWorkspaceFolder(target) ? ConfigurationTarget.WORKSPACE_FOLDER : target) ?? ConfigurationTarget.WORKSPACE, value);
520
}
521
}
522
523
const packageType = this.getPackageType(serverType);
524
if (packageType) {
525
this._telemetryService.publicLog2<AddServerCompletedData, AddServerCompletedClassification>('mcp.addserver.completed', {
526
packageType,
527
serverType: config.type,
528
target: target === ConfigurationTarget.WORKSPACE ? 'workspace' : 'user'
529
});
530
}
531
532
this.showOnceDiscovered(name);
533
}
534
535
public async pickForUrlHandler(resource: URI, showIsPrimary = false): Promise<void> {
536
const name = decodeURIComponent(basename(resource)).replace(/\.json$/, '');
537
const placeHolder = localize('install.title', 'Install MCP server {0}', name);
538
539
const items: IQuickPickItem[] = [
540
{ id: 'install', label: localize('install.start', 'Install Server') },
541
{ id: 'show', label: localize('install.show', 'Show Configuration', name) },
542
{ id: 'rename', label: localize('install.rename', 'Rename "{0}"', name) },
543
{ id: 'cancel', label: localize('cancel', 'Cancel') },
544
];
545
if (showIsPrimary) {
546
[items[0], items[1]] = [items[1], items[0]];
547
}
548
549
const pick = await this._quickInputService.pick(items, { placeHolder, ignoreFocusLost: true });
550
const getEditors = () => this._editorService.findEditors(resource);
551
552
switch (pick?.id) {
553
case 'show':
554
await this._editorService.openEditor({ resource });
555
break;
556
case 'install':
557
await this._editorService.save(getEditors());
558
try {
559
const contents = await this._fileService.readFile(resource);
560
const { inputs, ...config }: IMcpServerConfiguration & { inputs?: IMcpServerVariable[] } = parseJsonc(contents.value.toString());
561
await this._mcpManagementService.install({ name, config, inputs });
562
this._editorService.closeEditors(getEditors());
563
this.showOnceDiscovered(name);
564
} catch (e) {
565
this._notificationService.error(localize('install.error', 'Error installing MCP server {0}: {1}', name, e.message));
566
await this._editorService.openEditor({ resource });
567
}
568
break;
569
case 'rename': {
570
const newName = await this._quickInputService.input({ placeHolder: localize('install.newName', 'Enter new name'), value: name });
571
if (newName) {
572
const newURI = resource.with({ path: `/${encodeURIComponent(newName)}.json` });
573
await this._editorService.save(getEditors());
574
await this._fileService.move(resource, newURI);
575
return this.pickForUrlHandler(newURI, showIsPrimary);
576
}
577
break;
578
}
579
}
580
}
581
582
private getPackageType(serverType: AddConfigurationType): string | undefined {
583
switch (serverType) {
584
case AddConfigurationType.NpmPackage:
585
return 'npm';
586
case AddConfigurationType.PipPackage:
587
return 'pip';
588
case AddConfigurationType.NuGetPackage:
589
return 'nuget';
590
case AddConfigurationType.DockerImage:
591
return 'docker';
592
case AddConfigurationType.Stdio:
593
return 'stdio';
594
case AddConfigurationType.HTTP:
595
return 'sse';
596
default:
597
return undefined;
598
}
599
}
600
}
601
602
export class McpInstallFromManifestCommand {
603
constructor(
604
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
605
@IFileService private readonly _fileService: IFileService,
606
@IQuickInputService private readonly _quickInputService: IQuickInputService,
607
@INotificationService private readonly _notificationService: INotificationService,
608
@IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService,
609
@ILogService private readonly _logService: ILogService,
610
) { }
611
612
async run(): Promise<void> {
613
// Step 1: Open file dialog to select the manifest file
614
const result = await this._fileDialogService.showOpenDialog({
615
title: localize('mcp.installFromManifest.title', "Select MCP Server Manifest"),
616
filters: [{ name: localize('mcp.installFromManifest.filter', "MCP Manifest"), extensions: ['json'] }],
617
canSelectFiles: true,
618
canSelectMany: false,
619
openLabel: localize({ key: 'mcp.installFromManifest.openLabel', comment: ['&& denotes a mnemonic'] }, "&&Install")
620
});
621
622
if (!result?.[0]) {
623
return;
624
}
625
626
const manifestUri = result[0];
627
628
// Step 2: Read and parse the manifest file
629
let manifest: unknown;
630
try {
631
const contents = await this._fileService.readFile(manifestUri);
632
manifest = parseJsonc(contents.value.toString());
633
} catch (e) {
634
this._notificationService.error(localize('mcp.installFromManifest.readError', "Failed to read manifest file: {0}", e.message));
635
return;
636
}
637
638
if (!manifest || typeof manifest !== 'object') {
639
this._notificationService.error(localize('mcp.installFromManifest.invalidJson', "Invalid manifest file: expected a JSON object"));
640
return;
641
}
642
643
// Step 3: Validate and extract configuration from gallery manifest
644
const galleryManifest = manifest as IGalleryMcpServerConfiguration & { name?: string };
645
646
// Determine package type from manifest
647
let packageType: RegistryType;
648
if (Array.isArray(galleryManifest.packages) && galleryManifest.packages.length > 0) {
649
packageType = galleryManifest.packages[0].registryType;
650
} else if (Array.isArray(galleryManifest.remotes) && galleryManifest.remotes.length > 0) {
651
packageType = RegistryType.REMOTE;
652
} else {
653
this._notificationService.error(localize('mcp.installFromManifest.invalidManifest', "Invalid manifest: expected 'packages' or 'remotes' with at least one entry"));
654
return;
655
}
656
657
let config: IMcpServerConfiguration;
658
let inputs: IMcpServerVariable[] | undefined;
659
try {
660
const { mcpServerConfiguration, notices } = this._mcpManagementService.getMcpServerConfigurationFromManifest(galleryManifest, packageType);
661
config = mcpServerConfiguration.config;
662
inputs = mcpServerConfiguration.inputs;
663
664
if (notices.length > 0) {
665
this._logService.warn(`MCP Management Service: Warnings while installing the MCP server from ${manifestUri.path}`, notices);
666
}
667
} catch (e) {
668
this._notificationService.error(localize('mcp.installFromManifest.parseError', "Failed to parse manifest: {0}", e.message));
669
return;
670
}
671
672
// Step 4: Get server name from manifest or prompt user
673
let name = galleryManifest.name;
674
if (!name) {
675
name = await this._quickInputService.input({
676
title: localize('mcp.installFromManifest.serverId.title', "Enter Server ID"),
677
placeHolder: localize('mcp.installFromManifest.serverId.placeholder', "Unique identifier for this server"),
678
value: basename(manifestUri).replace(/\.json$/i, ''),
679
ignoreFocusLost: true,
680
});
681
682
if (!name) {
683
return;
684
}
685
}
686
687
// Step 5: Install to user settings
688
try {
689
await this._mcpManagementService.install({ name, config, inputs });
690
this._notificationService.info(localize('mcp.installFromManifest.success', "MCP server '{0}' installed successfully", name));
691
} catch (e) {
692
this._notificationService.error(localize('mcp.installFromManifest.installError', "Failed to install MCP server: {0}", e.message));
693
}
694
}
695
}
696
697