Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts
5281 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 { getDomNodePagePosition } from '../../../../base/browser/dom.js';
7
import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
8
import { alert } from '../../../../base/browser/ui/aria/aria.js';
9
import { Action, IAction, IActionChangeEvent, Separator } from '../../../../base/common/actions.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
12
import { disposeIfDisposable } from '../../../../base/common/lifecycle.js';
13
import { ThemeIcon } from '../../../../base/common/themables.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { localize } from '../../../../nls.js';
16
import { Location } from '../../../../editor/common/languages.js';
17
import { ICommandService } from '../../../../platform/commands/common/commands.js';
18
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
19
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
20
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
21
import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';
22
import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';
23
import { IEditorService } from '../../../services/editor/common/editorService.js';
24
import { errorIcon, infoIcon, manageExtensionIcon, trustIcon, warningIcon } from '../../extensions/browser/extensionsIcons.js';
25
import { McpCommandIds } from '../common/mcpCommandIds.js';
26
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
27
import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab, McpServerInstallState } from '../common/mcpTypes.js';
28
import { startServerByFilter } from '../common/mcpTypesUtils.js';
29
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
30
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
31
import { IQuickInputService, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
32
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
33
import { Schemas } from '../../../../base/common/network.js';
34
import { ILabelService } from '../../../../platform/label/common/label.js';
35
import { LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
36
import { ExtensionAction } from '../../extensions/browser/extensionsActions.js';
37
import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
38
import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js';
39
import Severity from '../../../../base/common/severity.js';
40
41
export interface IMcpServerActionChangeEvent extends IActionChangeEvent {
42
readonly hidden?: boolean;
43
readonly menuActions?: IAction[];
44
}
45
46
export abstract class McpServerAction extends Action implements IMcpServerContainer {
47
48
protected override _onDidChange = this._register(new Emitter<IMcpServerActionChangeEvent>());
49
override get onDidChange() { return this._onDidChange.event; }
50
51
static readonly EXTENSION_ACTION_CLASS = 'extension-action';
52
static readonly TEXT_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} text`;
53
static readonly LABEL_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} label`;
54
static readonly PROMINENT_LABEL_ACTION_CLASS = `${McpServerAction.LABEL_ACTION_CLASS} prominent`;
55
static readonly ICON_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} icon`;
56
57
private _hidden: boolean = false;
58
get hidden(): boolean { return this._hidden; }
59
set hidden(hidden: boolean) {
60
if (this._hidden !== hidden) {
61
this._hidden = hidden;
62
this._onDidChange.fire({ hidden });
63
}
64
}
65
66
protected override _setEnabled(value: boolean): void {
67
super._setEnabled(value);
68
if (this.hideOnDisabled) {
69
this.hidden = !value;
70
}
71
}
72
73
protected hideOnDisabled: boolean = true;
74
75
private _mcpServer: IWorkbenchMcpServer | null = null;
76
get mcpServer(): IWorkbenchMcpServer | null { return this._mcpServer; }
77
set mcpServer(mcpServer: IWorkbenchMcpServer | null) { this._mcpServer = mcpServer; this.update(); }
78
79
abstract update(): void;
80
}
81
82
export class ButtonWithDropDownExtensionAction extends McpServerAction {
83
84
private primaryAction: IAction | undefined;
85
86
readonly menuActionClassNames: string[] = [];
87
private _menuActions: IAction[] = [];
88
get menuActions(): IAction[] { return [...this._menuActions]; }
89
90
override get mcpServer(): IWorkbenchMcpServer | null {
91
return super.mcpServer;
92
}
93
94
override set mcpServer(mcpServer: IWorkbenchMcpServer | null) {
95
this.actions.forEach(a => a.mcpServer = mcpServer);
96
super.mcpServer = mcpServer;
97
}
98
99
protected readonly actions: McpServerAction[];
100
101
constructor(
102
id: string,
103
clazz: string,
104
private readonly actionsGroups: McpServerAction[][],
105
) {
106
clazz = `${clazz} action-dropdown`;
107
super(id, undefined, clazz);
108
this.menuActionClassNames = clazz.split(' ');
109
this.hideOnDisabled = false;
110
this.actions = actionsGroups.flat();
111
this.update();
112
this._register(Event.any(...this.actions.map(a => a.onDidChange))(() => this.update(true)));
113
this.actions.forEach(a => this._register(a));
114
}
115
116
update(donotUpdateActions?: boolean): void {
117
if (!donotUpdateActions) {
118
this.actions.forEach(a => a.update());
119
}
120
121
const actionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => !a.hidden));
122
123
let actions: IAction[] = [];
124
for (const visibleActions of actionsGroups) {
125
if (visibleActions.length) {
126
actions = [...actions, ...visibleActions, new Separator()];
127
}
128
}
129
actions = actions.length ? actions.slice(0, actions.length - 1) : actions;
130
131
this.primaryAction = actions[0];
132
this._menuActions = actions.length > 1 ? actions : [];
133
this._onDidChange.fire({ menuActions: this._menuActions });
134
135
if (this.primaryAction) {
136
this.enabled = this.primaryAction.enabled;
137
this.label = this.getLabel(this.primaryAction as ExtensionAction);
138
this.tooltip = this.primaryAction.tooltip;
139
} else {
140
this.enabled = false;
141
}
142
}
143
144
override async run(): Promise<void> {
145
if (this.enabled) {
146
await this.primaryAction?.run();
147
}
148
}
149
150
protected getLabel(action: ExtensionAction): string {
151
return action.label;
152
}
153
}
154
155
export class ButtonWithDropdownExtensionActionViewItem extends ActionWithDropdownActionViewItem {
156
157
constructor(
158
action: ButtonWithDropDownExtensionAction,
159
options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions,
160
contextMenuProvider: IContextMenuProvider
161
) {
162
super(null, action, options, contextMenuProvider);
163
this._register(action.onDidChange(e => {
164
if (e.hidden !== undefined || e.menuActions !== undefined) {
165
this.updateClass();
166
}
167
}));
168
}
169
170
override render(container: HTMLElement): void {
171
super.render(container);
172
this.updateClass();
173
}
174
175
protected override updateClass(): void {
176
super.updateClass();
177
if (this.element && this.dropdownMenuActionViewItem?.element) {
178
this.element.classList.toggle('hide', (<ButtonWithDropDownExtensionAction>this._action).hidden);
179
const isMenuEmpty = (<ButtonWithDropDownExtensionAction>this._action).menuActions.length === 0;
180
this.element.classList.toggle('empty', isMenuEmpty);
181
this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty);
182
}
183
}
184
185
}
186
187
export abstract class DropDownAction extends McpServerAction {
188
189
constructor(
190
id: string,
191
label: string,
192
cssClass: string,
193
enabled: boolean,
194
@IInstantiationService protected instantiationService: IInstantiationService
195
) {
196
super(id, label, cssClass, enabled);
197
}
198
199
private _actionViewItem: DropDownExtensionActionViewItem | null = null;
200
createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem {
201
this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options);
202
return this._actionViewItem;
203
}
204
205
public override run(actionGroups: IAction[][]): Promise<void> {
206
this._actionViewItem?.showMenu(actionGroups);
207
return Promise.resolve();
208
}
209
}
210
211
export class DropDownExtensionActionViewItem extends ActionViewItem {
212
213
constructor(
214
action: IAction,
215
options: IActionViewItemOptions,
216
@IContextMenuService private readonly contextMenuService: IContextMenuService
217
) {
218
super(null, action, { ...options, icon: true, label: true });
219
}
220
221
public showMenu(menuActionGroups: IAction[][]): void {
222
if (this.element) {
223
const actions = this.getActions(menuActionGroups);
224
const elementPosition = getDomNodePagePosition(this.element);
225
const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 };
226
this.contextMenuService.showContextMenu({
227
getAnchor: () => anchor,
228
getActions: () => actions,
229
actionRunner: this.actionRunner,
230
onHide: () => disposeIfDisposable(actions)
231
});
232
}
233
}
234
235
private getActions(menuActionGroups: IAction[][]): IAction[] {
236
let actions: IAction[] = [];
237
for (const menuActions of menuActionGroups) {
238
actions = [...actions, ...menuActions, new Separator()];
239
}
240
return actions.length ? actions.slice(0, actions.length - 1) : actions;
241
}
242
}
243
244
export class InstallAction extends McpServerAction {
245
246
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;
247
private static readonly HIDE = `${this.CLASS} hide`;
248
249
constructor(
250
private readonly open: boolean,
251
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
252
@ITelemetryService private readonly telemetryService: ITelemetryService,
253
@IMcpService private readonly mcpService: IMcpService,
254
) {
255
super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false);
256
this.update();
257
}
258
259
update(): void {
260
this.enabled = false;
261
this.class = InstallAction.HIDE;
262
if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {
263
return;
264
}
265
if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) {
266
return;
267
}
268
this.class = InstallAction.CLASS;
269
this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;
270
}
271
272
override async run(): Promise<void> {
273
if (!this.mcpServer) {
274
return;
275
}
276
277
if (this.open) {
278
this.mcpWorkbenchService.open(this.mcpServer);
279
alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));
280
}
281
282
type McpServerInstallClassification = {
283
owner: 'sandy081';
284
comment: 'Used to understand if the action to install the MCP server is used.';
285
name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };
286
};
287
type McpServerInstall = {
288
name?: string;
289
};
290
this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install', { name: this.mcpServer.gallery?.name });
291
292
const installed = await this.mcpWorkbenchService.install(this.mcpServer);
293
294
await startServerByFilter(this.mcpService, s => {
295
return s.definition.label === installed.name;
296
});
297
}
298
}
299
300
export class InstallInWorkspaceAction extends McpServerAction {
301
302
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;
303
private static readonly HIDE = `${this.CLASS} hide`;
304
305
constructor(
306
private readonly open: boolean,
307
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
308
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
309
@IQuickInputService private readonly quickInputService: IQuickInputService,
310
@ITelemetryService private readonly telemetryService: ITelemetryService,
311
@IMcpService private readonly mcpService: IMcpService,
312
) {
313
super('extensions.installWorkspace', localize('installInWorkspace', "Install in Workspace"), InstallAction.CLASS, false);
314
this.update();
315
}
316
317
update(): void {
318
this.enabled = false;
319
this.class = InstallInWorkspaceAction.HIDE;
320
if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) {
321
return;
322
}
323
if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {
324
return;
325
}
326
if (this.mcpServer.installState !== McpServerInstallState.Uninstalled && this.mcpServer.local?.scope === LocalMcpServerScope.Workspace) {
327
return;
328
}
329
this.class = InstallAction.CLASS;
330
this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;
331
}
332
333
override async run(): Promise<void> {
334
if (!this.mcpServer) {
335
return;
336
}
337
338
if (this.open) {
339
this.mcpWorkbenchService.open(this.mcpServer, { preserveFocus: true });
340
alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));
341
}
342
343
const target = await this.getConfigurationTarget();
344
if (!target) {
345
return;
346
}
347
348
type McpServerInstallClassification = {
349
owner: 'sandy081';
350
comment: 'Used to understand if the action to install the MCP server is used.';
351
name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };
352
};
353
type McpServerInstall = {
354
name?: string;
355
};
356
this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install:workspace', { name: this.mcpServer.gallery?.name });
357
358
const installed = await this.mcpWorkbenchService.install(this.mcpServer, { target });
359
await startServerByFilter(this.mcpService, s => {
360
return s.definition.label === installed.name;
361
});
362
}
363
364
private async getConfigurationTarget(): Promise<ConfigurationTarget | IWorkspaceFolder | undefined> {
365
type OptionQuickPickItem = QuickPickItem & { target?: ConfigurationTarget | IWorkspaceFolder };
366
const options: OptionQuickPickItem[] = [];
367
368
for (const folder of this.workspaceService.getWorkspace().folders) {
369
options.push({ target: folder, label: folder.name, description: localize('install in workspace folder', "Workspace Folder") });
370
}
371
372
if (this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
373
if (options.length > 0) {
374
options.push({ type: 'separator' });
375
}
376
options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace") });
377
}
378
379
if (options.length === 1) {
380
return options[0].target;
381
}
382
383
const targetPick = await this.quickInputService.pick(options, {
384
title: localize('mcp.target.title', "Choose where to install the MCP server"),
385
});
386
387
return (targetPick as OptionQuickPickItem)?.target;
388
}
389
}
390
391
export class InstallInRemoteAction extends McpServerAction {
392
393
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;
394
private static readonly HIDE = `${this.CLASS} hide`;
395
396
constructor(
397
private readonly open: boolean,
398
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
399
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
400
@ITelemetryService private readonly telemetryService: ITelemetryService,
401
@ILabelService private readonly labelService: ILabelService,
402
@IMcpService private readonly mcpService: IMcpService,
403
) {
404
super('extensions.installRemote', localize('installInRemote', "Install (Remote)"), InstallAction.CLASS, false);
405
const remoteLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority);
406
this.label = localize('installInRemoteLabel', "Install in {0}", remoteLabel);
407
this.update();
408
}
409
410
update(): void {
411
this.enabled = false;
412
this.class = InstallInRemoteAction.HIDE;
413
if (!this.environmentService.remoteAuthority) {
414
return;
415
}
416
if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {
417
return;
418
}
419
if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) {
420
if (this.mcpServer.local?.scope === LocalMcpServerScope.RemoteUser) {
421
return;
422
}
423
if (this.mcpWorkbenchService.local.find(mcpServer => mcpServer.name === this.mcpServer?.name && mcpServer.local?.scope === LocalMcpServerScope.RemoteUser)) {
424
return;
425
}
426
}
427
this.class = InstallAction.CLASS;
428
this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;
429
}
430
431
override async run(): Promise<void> {
432
if (!this.mcpServer) {
433
return;
434
}
435
436
if (this.open) {
437
this.mcpWorkbenchService.open(this.mcpServer);
438
alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));
439
}
440
441
type McpServerInstallClassification = {
442
owner: 'sandy081';
443
comment: 'Used to understand if the action to install the MCP server is used.';
444
name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };
445
};
446
type McpServerInstall = {
447
name?: string;
448
};
449
this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install:remote', { name: this.mcpServer.gallery?.name });
450
451
const installed = await this.mcpWorkbenchService.install(this.mcpServer, { target: ConfigurationTarget.USER_REMOTE });
452
await startServerByFilter(this.mcpService, s => {
453
return s.definition.label === installed.name;
454
});
455
}
456
457
}
458
459
export class InstallingLabelAction extends McpServerAction {
460
461
private static readonly LABEL = localize('installing', "Installing");
462
private static readonly CLASS = `${McpServerAction.LABEL_ACTION_CLASS} install installing`;
463
464
constructor() {
465
super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false);
466
}
467
468
update(): void {
469
this.class = `${InstallingLabelAction.CLASS}${this.mcpServer && this.mcpServer.installState === McpServerInstallState.Installing ? '' : ' hide'}`;
470
}
471
}
472
473
export class UninstallAction extends McpServerAction {
474
475
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent uninstall`;
476
private static readonly HIDE = `${this.CLASS} hide`;
477
478
constructor(
479
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
480
) {
481
super('extensions.uninstall', localize('uninstall', "Uninstall"), UninstallAction.CLASS, false);
482
this.update();
483
}
484
485
update(): void {
486
this.enabled = false;
487
this.class = UninstallAction.HIDE;
488
if (!this.mcpServer) {
489
return;
490
}
491
if (!this.mcpServer.local) {
492
return;
493
}
494
if (this.mcpServer.installState !== McpServerInstallState.Installed) {
495
this.enabled = false;
496
return;
497
}
498
this.class = UninstallAction.CLASS;
499
this.enabled = true;
500
this.label = localize('uninstall', "Uninstall");
501
}
502
503
override async run(): Promise<void> {
504
if (!this.mcpServer) {
505
return;
506
}
507
await this.mcpWorkbenchService.uninstall(this.mcpServer);
508
}
509
}
510
511
export function getContextMenuActions(mcpServer: IWorkbenchMcpServer, isEditorAction: boolean, instantiationService: IInstantiationService): IAction[][] {
512
return instantiationService.invokeFunction(accessor => {
513
const workspaceService = accessor.get(IWorkspaceContextService);
514
const environmentService = accessor.get(IWorkbenchEnvironmentService);
515
516
const groups: McpServerAction[][] = [];
517
const isInstalled = mcpServer.installState === McpServerInstallState.Installed;
518
519
if (isInstalled) {
520
groups.push([
521
instantiationService.createInstance(StartServerAction),
522
]);
523
groups.push([
524
instantiationService.createInstance(StopServerAction),
525
instantiationService.createInstance(RestartServerAction),
526
]);
527
groups.push([
528
instantiationService.createInstance(AuthServerAction),
529
]);
530
groups.push([
531
instantiationService.createInstance(ShowServerOutputAction),
532
instantiationService.createInstance(ShowServerConfigurationAction),
533
instantiationService.createInstance(ShowServerJsonConfigurationAction),
534
]);
535
groups.push([
536
instantiationService.createInstance(ConfigureModelAccessAction),
537
instantiationService.createInstance(ShowSamplingRequestsAction),
538
]);
539
groups.push([
540
instantiationService.createInstance(BrowseResourcesAction),
541
]);
542
if (!isEditorAction) {
543
const installGroup: McpServerAction[] = [instantiationService.createInstance(UninstallAction)];
544
if (workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY) {
545
installGroup.push(instantiationService.createInstance(InstallInWorkspaceAction, false));
546
}
547
if (environmentService.remoteAuthority && mcpServer.local?.scope !== LocalMcpServerScope.RemoteUser) {
548
installGroup.push(instantiationService.createInstance(InstallInRemoteAction, false));
549
}
550
groups.push(installGroup);
551
}
552
} else {
553
const installGroup = [];
554
if (workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY) {
555
installGroup.push(instantiationService.createInstance(InstallInWorkspaceAction, !isEditorAction));
556
}
557
if (environmentService.remoteAuthority) {
558
installGroup.push(instantiationService.createInstance(InstallInRemoteAction, !isEditorAction));
559
}
560
groups.push(installGroup);
561
}
562
groups.forEach(group => group.forEach(extensionAction => extensionAction.mcpServer = mcpServer));
563
564
return groups;
565
});
566
}
567
568
export class ManageMcpServerAction extends DropDownAction {
569
570
static readonly ID = 'mcpServer.manage';
571
572
private static readonly Class = `${McpServerAction.ICON_ACTION_CLASS} manage ` + ThemeIcon.asClassName(manageExtensionIcon);
573
private static readonly HideManageExtensionClass = `${this.Class} hide`;
574
575
constructor(
576
private readonly isEditorAction: boolean,
577
@IInstantiationService instantiationService: IInstantiationService,
578
) {
579
580
super(ManageMcpServerAction.ID, '', '', true, instantiationService);
581
this.tooltip = localize('manage', "Manage");
582
this.update();
583
}
584
585
override async run(): Promise<void> {
586
return super.run(this.mcpServer ? getContextMenuActions(this.mcpServer, this.isEditorAction, this.instantiationService) : []);
587
}
588
589
update(): void {
590
this.class = ManageMcpServerAction.HideManageExtensionClass;
591
this.enabled = false;
592
if (!this.mcpServer) {
593
return;
594
}
595
if (this.isEditorAction) {
596
this.enabled = true;
597
this.class = ManageMcpServerAction.Class;
598
} else {
599
this.enabled = !!this.mcpServer.local;
600
this.class = this.enabled ? ManageMcpServerAction.Class : ManageMcpServerAction.HideManageExtensionClass;
601
}
602
}
603
}
604
605
export class StartServerAction extends McpServerAction {
606
607
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent start`;
608
private static readonly HIDE = `${this.CLASS} hide`;
609
610
constructor(
611
@IMcpService private readonly mcpService: IMcpService,
612
) {
613
super('extensions.start', localize('start', "Start Server"), StartServerAction.CLASS, false);
614
this.update();
615
}
616
617
update(): void {
618
this.enabled = false;
619
this.class = StartServerAction.HIDE;
620
const server = this.getServer();
621
if (!server) {
622
return;
623
}
624
const serverState = server.connectionState.get();
625
if (!McpConnectionState.canBeStarted(serverState.state)) {
626
return;
627
}
628
this.class = StartServerAction.CLASS;
629
this.enabled = true;
630
this.label = localize('start', "Start Server");
631
}
632
633
override async run(): Promise<void> {
634
const server = this.getServer();
635
if (!server) {
636
return;
637
}
638
await server.start({ promptType: 'all-untrusted' });
639
server.showOutput();
640
}
641
642
private getServer(): IMcpServer | undefined {
643
if (!this.mcpServer) {
644
return;
645
}
646
if (!this.mcpServer.local) {
647
return;
648
}
649
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
650
}
651
}
652
653
export class StopServerAction extends McpServerAction {
654
655
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent stop`;
656
private static readonly HIDE = `${this.CLASS} hide`;
657
658
constructor(
659
@IMcpService private readonly mcpService: IMcpService,
660
) {
661
super('extensions.stop', localize('stop', "Stop Server"), StopServerAction.CLASS, false);
662
this.update();
663
}
664
665
update(): void {
666
this.enabled = false;
667
this.class = StopServerAction.HIDE;
668
const server = this.getServer();
669
if (!server) {
670
return;
671
}
672
const serverState = server.connectionState.get();
673
if (McpConnectionState.canBeStarted(serverState.state)) {
674
return;
675
}
676
this.class = StopServerAction.CLASS;
677
this.enabled = true;
678
this.label = localize('stop', "Stop Server");
679
}
680
681
override async run(): Promise<void> {
682
const server = this.getServer();
683
if (!server) {
684
return;
685
}
686
await server.stop();
687
}
688
689
private getServer(): IMcpServer | undefined {
690
if (!this.mcpServer) {
691
return;
692
}
693
if (!this.mcpServer.local) {
694
return;
695
}
696
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
697
}
698
}
699
700
export class RestartServerAction extends McpServerAction {
701
702
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent restart`;
703
private static readonly HIDE = `${this.CLASS} hide`;
704
705
constructor(
706
@IMcpService private readonly mcpService: IMcpService,
707
) {
708
super('extensions.restart', localize('restart', "Restart Server"), RestartServerAction.CLASS, false);
709
this.update();
710
}
711
712
update(): void {
713
this.enabled = false;
714
this.class = RestartServerAction.HIDE;
715
const server = this.getServer();
716
if (!server) {
717
return;
718
}
719
const serverState = server.connectionState.get();
720
if (McpConnectionState.canBeStarted(serverState.state)) {
721
return;
722
}
723
this.class = RestartServerAction.CLASS;
724
this.enabled = true;
725
this.label = localize('restart', "Restart Server");
726
}
727
728
override async run(): Promise<void> {
729
const server = this.getServer();
730
if (!server) {
731
return;
732
}
733
await server.stop();
734
await server.start({ promptType: 'all-untrusted' });
735
server.showOutput();
736
}
737
738
private getServer(): IMcpServer | undefined {
739
if (!this.mcpServer) {
740
return;
741
}
742
if (!this.mcpServer.local) {
743
return;
744
}
745
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
746
}
747
}
748
749
export class AuthServerAction extends McpServerAction {
750
751
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent account`;
752
private static readonly HIDE = `${this.CLASS} hide`;
753
754
private static readonly SIGN_OUT = localize('mcp.signOut', 'Sign Out');
755
private static readonly DISCONNECT = localize('mcp.disconnect', 'Disconnect Account');
756
757
private _accountQuery: IAccountQuery | undefined;
758
759
constructor(
760
@IMcpService private readonly mcpService: IMcpService,
761
@IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService,
762
@IAuthenticationService private readonly _authenticationService: IAuthenticationService
763
) {
764
super('extensions.restart', localize('restart', "Restart Server"), RestartServerAction.CLASS, false);
765
this.update();
766
}
767
768
update(): void {
769
this.enabled = false;
770
this.class = AuthServerAction.HIDE;
771
const server = this.getServer();
772
if (!server) {
773
return;
774
}
775
const accountQuery = this.getAccountQuery();
776
if (!accountQuery) {
777
return;
778
}
779
this._accountQuery = accountQuery;
780
this.class = AuthServerAction.CLASS;
781
this.enabled = true;
782
let label = accountQuery.entities().getEntityCount().total > 1 ? AuthServerAction.DISCONNECT : AuthServerAction.SIGN_OUT;
783
label += ` (${accountQuery.accountName})`;
784
this.label = label;
785
}
786
787
override async run(): Promise<void> {
788
const server = this.getServer();
789
if (!server) {
790
return;
791
}
792
const accountQuery = this.getAccountQuery();
793
if (!accountQuery) {
794
return;
795
}
796
await server.stop();
797
const { providerId, accountName } = accountQuery;
798
accountQuery.mcpServer(server.definition.id).setAccessAllowed(false, server.definition.label);
799
if (this.label === AuthServerAction.SIGN_OUT) {
800
const accounts = await this._authenticationService.getAccounts(providerId);
801
const account = accounts.find(a => a.label === accountName);
802
if (account) {
803
const sessions = await this._authenticationService.getSessions(providerId, undefined, { account });
804
for (const session of sessions) {
805
await this._authenticationService.removeSession(providerId, session.id);
806
}
807
}
808
}
809
}
810
811
private getServer(): IMcpServer | undefined {
812
if (!this.mcpServer) {
813
return;
814
}
815
if (!this.mcpServer.local) {
816
return;
817
}
818
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
819
}
820
821
private getAccountQuery(): IAccountQuery | undefined {
822
const server = this.getServer();
823
if (!server) {
824
return undefined;
825
}
826
if (this._accountQuery) {
827
return this._accountQuery;
828
}
829
const serverId = server.definition.id;
830
const preferences = this._authenticationQueryService.mcpServer(serverId).getAllAccountPreferences();
831
if (!preferences.size) {
832
return undefined;
833
}
834
for (const [providerId, accountName] of preferences) {
835
const accountQuery = this._authenticationQueryService.provider(providerId).account(accountName);
836
if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {
837
continue; // skip accounts that are not allowed
838
}
839
return accountQuery;
840
}
841
return undefined;
842
}
843
844
}
845
846
export class ShowServerOutputAction extends McpServerAction {
847
848
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent output`;
849
private static readonly HIDE = `${this.CLASS} hide`;
850
851
constructor(
852
@IMcpService private readonly mcpService: IMcpService,
853
) {
854
super('extensions.output', localize('output', "Show Output"), ShowServerOutputAction.CLASS, false);
855
this.update();
856
}
857
858
update(): void {
859
this.enabled = false;
860
this.class = ShowServerOutputAction.HIDE;
861
const server = this.getServer();
862
if (!server) {
863
return;
864
}
865
this.class = ShowServerOutputAction.CLASS;
866
this.enabled = true;
867
this.label = localize('output', "Show Output");
868
}
869
870
override async run(): Promise<void> {
871
const server = this.getServer();
872
if (!server) {
873
return;
874
}
875
server.showOutput();
876
}
877
878
private getServer(): IMcpServer | undefined {
879
if (!this.mcpServer) {
880
return;
881
}
882
if (!this.mcpServer.local) {
883
return;
884
}
885
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
886
}
887
}
888
889
export class ShowServerConfigurationAction extends McpServerAction {
890
891
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;
892
private static readonly HIDE = `${this.CLASS} hide`;
893
894
constructor(
895
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService
896
) {
897
super('extensions.config', localize('config', "Show Configuration"), ShowServerConfigurationAction.CLASS, false);
898
this.update();
899
}
900
901
update(): void {
902
this.enabled = false;
903
this.class = ShowServerConfigurationAction.HIDE;
904
if (!this.mcpServer?.local) {
905
return;
906
}
907
this.class = ShowServerConfigurationAction.CLASS;
908
this.enabled = true;
909
}
910
911
override async run(): Promise<void> {
912
if (!this.mcpServer?.local) {
913
return;
914
}
915
this.mcpWorkbenchService.open(this.mcpServer, { tab: McpServerEditorTab.Configuration });
916
}
917
918
}
919
920
export class ShowServerJsonConfigurationAction extends McpServerAction {
921
922
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;
923
private static readonly HIDE = `${this.CLASS} hide`;
924
925
constructor(
926
@IMcpService private readonly mcpService: IMcpService,
927
@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,
928
@IEditorService private readonly editorService: IEditorService,
929
) {
930
super('extensions.jsonConfig', localize('configJson', "Show Configuration (JSON)"), ShowServerJsonConfigurationAction.CLASS, false);
931
this.update();
932
}
933
934
update(): void {
935
this.enabled = false;
936
this.class = ShowServerJsonConfigurationAction.HIDE;
937
const configurationTarget = this.getConfigurationTarget();
938
if (!configurationTarget) {
939
return;
940
}
941
this.class = ShowServerConfigurationAction.CLASS;
942
this.enabled = true;
943
}
944
945
override async run(): Promise<void> {
946
const configurationTarget = this.getConfigurationTarget();
947
if (!configurationTarget) {
948
return;
949
}
950
this.editorService.openEditor({
951
resource: URI.isUri(configurationTarget) ? configurationTarget : configurationTarget!.uri,
952
options: { selection: URI.isUri(configurationTarget) ? undefined : configurationTarget!.range }
953
});
954
}
955
956
private getConfigurationTarget(): Location | URI | undefined {
957
if (!this.mcpServer) {
958
return;
959
}
960
if (!this.mcpServer.local) {
961
return;
962
}
963
const server = this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name);
964
if (!server) {
965
return;
966
}
967
const collection = this.mcpRegistry.collections.get().find(c => c.id === server.collection.id);
968
const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);
969
return serverDefinition?.presentation?.origin || collection?.presentation?.origin;
970
}
971
}
972
973
export class ConfigureModelAccessAction extends McpServerAction {
974
975
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;
976
private static readonly HIDE = `${this.CLASS} hide`;
977
978
constructor(
979
@IMcpService private readonly mcpService: IMcpService,
980
@ICommandService private readonly commandService: ICommandService,
981
) {
982
super('extensions.config', localize('mcp.configAccess', 'Configure Model Access'), ConfigureModelAccessAction.CLASS, false);
983
this.update();
984
}
985
986
update(): void {
987
this.enabled = false;
988
this.class = ConfigureModelAccessAction.HIDE;
989
const server = this.getServer();
990
if (!server) {
991
return;
992
}
993
this.class = ConfigureModelAccessAction.CLASS;
994
this.enabled = true;
995
this.label = localize('mcp.configAccess', 'Configure Model Access');
996
}
997
998
override async run(): Promise<void> {
999
const server = this.getServer();
1000
if (!server) {
1001
return;
1002
}
1003
this.commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);
1004
}
1005
1006
private getServer(): IMcpServer | undefined {
1007
if (!this.mcpServer) {
1008
return;
1009
}
1010
if (!this.mcpServer.local) {
1011
return;
1012
}
1013
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
1014
}
1015
}
1016
1017
export class ShowSamplingRequestsAction extends McpServerAction {
1018
1019
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;
1020
private static readonly HIDE = `${this.CLASS} hide`;
1021
1022
constructor(
1023
@IMcpService private readonly mcpService: IMcpService,
1024
@IMcpSamplingService private readonly samplingService: IMcpSamplingService,
1025
@IEditorService private readonly editorService: IEditorService,
1026
) {
1027
super('extensions.config', localize('mcp.samplingLog', 'Show Sampling Requests'), ShowSamplingRequestsAction.CLASS, false);
1028
this.update();
1029
}
1030
1031
update(): void {
1032
this.enabled = false;
1033
this.class = ShowSamplingRequestsAction.HIDE;
1034
const server = this.getServer();
1035
if (!server) {
1036
return;
1037
}
1038
if (!this.samplingService.hasLogs(server)) {
1039
return;
1040
}
1041
this.class = ShowSamplingRequestsAction.CLASS;
1042
this.enabled = true;
1043
}
1044
1045
override async run(): Promise<void> {
1046
const server = this.getServer();
1047
if (!server) {
1048
return;
1049
}
1050
if (!this.samplingService.hasLogs(server)) {
1051
return;
1052
}
1053
this.editorService.openEditor({
1054
resource: undefined,
1055
contents: this.samplingService.getLogText(server),
1056
label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),
1057
});
1058
}
1059
1060
private getServer(): IMcpServer | undefined {
1061
if (!this.mcpServer) {
1062
return;
1063
}
1064
if (!this.mcpServer.local) {
1065
return;
1066
}
1067
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
1068
}
1069
}
1070
1071
export class BrowseResourcesAction extends McpServerAction {
1072
1073
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;
1074
private static readonly HIDE = `${this.CLASS} hide`;
1075
1076
constructor(
1077
@IMcpService private readonly mcpService: IMcpService,
1078
@ICommandService private readonly commandService: ICommandService,
1079
) {
1080
super('extensions.config', localize('mcp.resources', 'Browse Resources'), BrowseResourcesAction.CLASS, false);
1081
this.update();
1082
}
1083
1084
update(): void {
1085
this.enabled = false;
1086
this.class = BrowseResourcesAction.HIDE;
1087
const server = this.getServer();
1088
if (!server) {
1089
return;
1090
}
1091
const capabilities = server.capabilities.get();
1092
if (capabilities !== undefined && !(capabilities & McpCapability.Resources)) {
1093
return;
1094
}
1095
this.class = BrowseResourcesAction.CLASS;
1096
this.enabled = true;
1097
}
1098
1099
override async run(): Promise<void> {
1100
const server = this.getServer();
1101
if (!server) {
1102
return;
1103
}
1104
const capabilities = server.capabilities.get();
1105
if (capabilities !== undefined && !(capabilities & McpCapability.Resources)) {
1106
return;
1107
}
1108
return this.commandService.executeCommand(McpCommandIds.BrowseResources, server);
1109
}
1110
1111
private getServer(): IMcpServer | undefined {
1112
if (!this.mcpServer) {
1113
return;
1114
}
1115
if (!this.mcpServer.local) {
1116
return;
1117
}
1118
return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);
1119
}
1120
}
1121
1122
export type McpServerStatus = { readonly message: IMarkdownString; readonly icon?: ThemeIcon };
1123
1124
export class McpServerStatusAction extends McpServerAction {
1125
1126
private static readonly CLASS = `${McpServerAction.ICON_ACTION_CLASS} extension-status`;
1127
1128
private _status: McpServerStatus[] = [];
1129
get status(): McpServerStatus[] { return this._status; }
1130
1131
private readonly _onDidChangeStatus = this._register(new Emitter<void>());
1132
readonly onDidChangeStatus = this._onDidChangeStatus.event;
1133
1134
constructor(
1135
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
1136
@ICommandService private readonly commandService: ICommandService,
1137
) {
1138
super('extensions.status', '', `${McpServerStatusAction.CLASS} hide`, false);
1139
this.update();
1140
}
1141
1142
update(): void {
1143
this.computeAndUpdateStatus();
1144
}
1145
1146
private computeAndUpdateStatus(): void {
1147
this.updateStatus(undefined, true);
1148
this.enabled = false;
1149
1150
if (!this.mcpServer) {
1151
return;
1152
}
1153
1154
if ((this.mcpServer.gallery || this.mcpServer.installable) && this.mcpServer.installState === McpServerInstallState.Uninstalled) {
1155
const result = this.mcpWorkbenchService.canInstall(this.mcpServer);
1156
if (result !== true) {
1157
this.updateStatus({ icon: warningIcon, message: result }, true);
1158
return;
1159
}
1160
}
1161
1162
const runtimeState = this.mcpServer.runtimeStatus;
1163
if (runtimeState?.message) {
1164
this.updateStatus({ icon: runtimeState.message.severity === Severity.Warning ? warningIcon : runtimeState.message.severity === Severity.Error ? errorIcon : infoIcon, message: runtimeState.message.text }, true);
1165
}
1166
}
1167
1168
private updateStatus(status: McpServerStatus | undefined, updateClass: boolean): void {
1169
if (status) {
1170
if (this._status.some(s => s.message.value === status.message.value && s.icon?.id === status.icon?.id)) {
1171
return;
1172
}
1173
} else {
1174
if (this._status.length === 0) {
1175
return;
1176
}
1177
this._status = [];
1178
}
1179
1180
if (status) {
1181
this._status.push(status);
1182
this._status.sort((a, b) =>
1183
b.icon === trustIcon ? -1 :
1184
a.icon === trustIcon ? 1 :
1185
b.icon === errorIcon ? -1 :
1186
a.icon === errorIcon ? 1 :
1187
b.icon === warningIcon ? -1 :
1188
a.icon === warningIcon ? 1 :
1189
b.icon === infoIcon ? -1 :
1190
a.icon === infoIcon ? 1 :
1191
0
1192
);
1193
}
1194
1195
if (updateClass) {
1196
if (status?.icon === errorIcon) {
1197
this.class = `${McpServerStatusAction.CLASS} extension-status-error ${ThemeIcon.asClassName(errorIcon)}`;
1198
}
1199
else if (status?.icon === warningIcon) {
1200
this.class = `${McpServerStatusAction.CLASS} extension-status-warning ${ThemeIcon.asClassName(warningIcon)}`;
1201
}
1202
else if (status?.icon === infoIcon) {
1203
this.class = `${McpServerStatusAction.CLASS} extension-status-info ${ThemeIcon.asClassName(infoIcon)}`;
1204
}
1205
else if (status?.icon === trustIcon) {
1206
this.class = `${McpServerStatusAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`;
1207
}
1208
else {
1209
this.class = `${McpServerStatusAction.CLASS} hide`;
1210
}
1211
}
1212
this._onDidChangeStatus.fire();
1213
}
1214
1215
override async run(): Promise<void> {
1216
if (this._status[0]?.icon === trustIcon) {
1217
return this.commandService.executeCommand('workbench.trust.manage');
1218
}
1219
}
1220
}
1221
1222