Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts
3296 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 './media/mcpServersView.css';
7
import * as dom from '../../../../base/browser/dom.js';
8
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
9
import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
12
import { DelayedPagedModel, IPagedModel, PagedModel } from '../../../../base/common/paging.js';
13
import { localize, localize2 } from '../../../../nls.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
17
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
20
import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';
21
import { INotificationService } from '../../../../platform/notification/common/notification.js';
22
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
23
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
24
import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js';
25
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
26
import { IViewDescriptorService, IViewsRegistry, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js';
27
import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, McpServerEnablementState, McpServerInstallState } from '../common/mcpTypes.js';
28
import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction, McpServerStatusAction } from './mcpServerActions.js';
29
import { PublisherWidget, StarredWidget, McpServerIconWidget, McpServerHoverWidget, McpServerScopeBadgeWidget } from './mcpServerWidgets.js';
30
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
31
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
32
import { IAllowedMcpServersService } from '../../../../platform/mcp/common/mcpManagement.js';
33
import { URI } from '../../../../base/common/uri.js';
34
import { ThemeIcon } from '../../../../base/common/themables.js';
35
import { IProductService } from '../../../../platform/product/common/productService.js';
36
import { Registry } from '../../../../platform/registry/common/platform.js';
37
import { IWorkbenchContribution } from '../../../common/contributions.js';
38
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
39
import { DefaultViewsContext, SearchMcpServersContext } from '../../extensions/common/extensions.js';
40
import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';
41
import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';
42
import { MarkdownString } from '../../../../base/common/htmlContent.js';
43
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
44
import { Button } from '../../../../base/browser/ui/button/button.js';
45
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
46
import { AbstractExtensionsListView } from '../../extensions/browser/extensionsViews.js';
47
import { ExtensionListRendererOptions } from '../../extensions/browser/extensionsList.js';
48
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
49
import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js';
50
import { mcpServerIcon } from './mcpServerIcons.js';
51
import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js';
52
import { IMcpGalleryManifestService, McpGalleryManifestStatus } from '../../../../platform/mcp/common/mcpGalleryManifest.js';
53
54
export interface McpServerListViewOptions {
55
showWelcomeOnEmpty?: boolean;
56
}
57
58
interface IQueryResult {
59
model: IPagedModel<IWorkbenchMcpServer>;
60
disposables: DisposableStore;
61
showWelcomeContent?: boolean;
62
onDidChangeModel?: Event<IPagedModel<IWorkbenchMcpServer>>;
63
}
64
65
export class McpServersListView extends AbstractExtensionsListView<IWorkbenchMcpServer> {
66
67
private list: WorkbenchPagedList<IWorkbenchMcpServer> | null = null;
68
private listContainer: HTMLElement | null = null;
69
private welcomeContainer: HTMLElement | null = null;
70
private readonly contextMenuActionRunner = this._register(new ActionRunner());
71
private input: IQueryResult | undefined;
72
73
constructor(
74
private readonly mpcViewOptions: McpServerListViewOptions,
75
options: IViewletViewOptions,
76
@IKeybindingService keybindingService: IKeybindingService,
77
@IContextMenuService contextMenuService: IContextMenuService,
78
@IInstantiationService instantiationService: IInstantiationService,
79
@IThemeService themeService: IThemeService,
80
@IHoverService hoverService: IHoverService,
81
@IConfigurationService configurationService: IConfigurationService,
82
@IContextKeyService contextKeyService: IContextKeyService,
83
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
84
@IOpenerService openerService: IOpenerService,
85
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
86
@IMcpGalleryManifestService protected readonly mcpGalleryManifestService: IMcpGalleryManifestService,
87
@IProductService private readonly productService: IProductService,
88
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
89
) {
90
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
91
}
92
93
protected override renderBody(container: HTMLElement): void {
94
super.renderBody(container);
95
96
// Create welcome container
97
this.welcomeContainer = dom.append(container, dom.$('.mcp-welcome-container.hide'));
98
this.createWelcomeContent(this.welcomeContainer);
99
100
this.listContainer = dom.append(container, dom.$('.mcp-servers-list'));
101
this.list = this._register(this.instantiationService.createInstance(WorkbenchPagedList,
102
`${this.id}-MCP-Servers`,
103
this.listContainer,
104
{
105
getHeight() { return 72; },
106
getTemplateId: () => McpServerRenderer.templateId,
107
},
108
[this.instantiationService.createInstance(McpServerRenderer, {
109
hoverOptions: {
110
position: () => {
111
const viewLocation = this.viewDescriptorService.getViewLocationById(this.id);
112
if (viewLocation === ViewContainerLocation.Sidebar) {
113
return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT;
114
}
115
if (viewLocation === ViewContainerLocation.AuxiliaryBar) {
116
return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT;
117
}
118
return HoverPosition.RIGHT;
119
}
120
}
121
})],
122
{
123
multipleSelectionSupport: false,
124
setRowLineHeight: false,
125
horizontalScrolling: false,
126
accessibilityProvider: {
127
getAriaLabel(mcpServer: IWorkbenchMcpServer | null): string {
128
return mcpServer?.label ?? '';
129
},
130
getWidgetAriaLabel(): string {
131
return localize('mcp servers', "MCP Servers");
132
}
133
},
134
overrideStyles: getLocationBasedViewColors(this.viewDescriptorService.getViewLocationById(this.id)).listOverrideStyles,
135
openOnSingleClick: true,
136
}) as WorkbenchPagedList<IWorkbenchMcpServer>);
137
this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => {
138
this.mcpWorkbenchService.open(options.element!, options.editorOptions);
139
}));
140
this._register(this.list.onContextMenu(e => this.onContextMenu(e), this));
141
142
if (this.input) {
143
this.renderInput();
144
}
145
}
146
147
private async onContextMenu(e: IListContextMenuEvent<IWorkbenchMcpServer>): Promise<void> {
148
if (e.element) {
149
const disposables = new DisposableStore();
150
const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageMcpServerAction, false));
151
const extension = e.element ? this.mcpWorkbenchService.local.find(local => local.id === e.element!.id) || e.element
152
: e.element;
153
manageExtensionAction.mcpServer = extension;
154
let groups: IAction[][] = [];
155
if (manageExtensionAction.enabled) {
156
groups = await manageExtensionAction.getActionGroups();
157
}
158
const actions: IAction[] = [];
159
for (const menuActions of groups) {
160
for (const menuAction of menuActions) {
161
actions.push(menuAction);
162
if (isDisposable(menuAction)) {
163
disposables.add(menuAction);
164
}
165
}
166
actions.push(new Separator());
167
}
168
actions.pop();
169
this.contextMenuService.showContextMenu({
170
getAnchor: () => e.anchor,
171
getActions: () => actions,
172
actionRunner: this.contextMenuActionRunner,
173
onHide: () => disposables.dispose()
174
});
175
}
176
}
177
178
protected override layoutBody(height: number, width: number): void {
179
super.layoutBody(height, width);
180
this.list?.layout(height, width);
181
}
182
183
async show(query: string): Promise<IPagedModel<IWorkbenchMcpServer>> {
184
if (this.input) {
185
this.input.disposables.dispose();
186
this.input = undefined;
187
}
188
189
this.input = await this.query(query.trim());
190
191
this.input.showWelcomeContent = !!this.mpcViewOptions.showWelcomeOnEmpty && this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Unavailable && this.input.model.length === 0;
192
this.renderInput();
193
194
if (this.input.onDidChangeModel) {
195
this.input.disposables.add(this.input.onDidChangeModel(model => {
196
if (!this.input) {
197
return;
198
}
199
this.input.model = model;
200
this.input.showWelcomeContent = !!this.mpcViewOptions.showWelcomeOnEmpty && this.mcpGalleryManifestService.mcpGalleryManifestStatus === McpGalleryManifestStatus.Unavailable && this.input.model.length === 0;
201
this.renderInput();
202
}));
203
}
204
205
return this.input.model;
206
}
207
208
private renderInput() {
209
if (!this.input) {
210
return;
211
}
212
if (this.list) {
213
this.list.model = new DelayedPagedModel(this.input.model);
214
}
215
this.showWelcomeContent(!!this.input.showWelcomeContent);
216
}
217
218
private showWelcomeContent(show: boolean): void {
219
this.welcomeContainer?.classList.toggle('hide', !show);
220
this.listContainer?.classList.toggle('hide', show);
221
}
222
223
private createWelcomeContent(welcomeContainer: HTMLElement): void {
224
const welcomeContent = dom.append(welcomeContainer, dom.$('.mcp-welcome-content'));
225
226
const iconContainer = dom.append(welcomeContent, dom.$('.mcp-welcome-icon'));
227
const iconElement = dom.append(iconContainer, dom.$('span'));
228
iconElement.className = ThemeIcon.asClassName(mcpServerIcon);
229
230
const title = dom.append(welcomeContent, dom.$('.mcp-welcome-title'));
231
title.textContent = localize('mcp.welcome.title', "MCP Servers");
232
233
const description = dom.append(welcomeContent, dom.$('.mcp-welcome-description'));
234
const markdownResult = this._register(renderMarkdown(new MarkdownString(
235
localize('mcp.welcome.descriptionWithLink', "Extend agent mode by installing MCP servers to bring extra tools for connecting to databases, invoking APIs and performing specialized tasks."),
236
{ isTrusted: true }
237
), {
238
actionHandler: (content: string) => {
239
this.openerService.open(URI.parse(content));
240
}
241
}));
242
description.appendChild(markdownResult.element);
243
244
// Browse button
245
const buttonContainer = dom.append(welcomeContent, dom.$('.mcp-welcome-button-container'));
246
const button = this._register(new Button(buttonContainer, {
247
title: localize('mcp.welcome.browseButton', "Browse MCP Servers"),
248
...defaultButtonStyles
249
}));
250
button.label = localize('mcp.welcome.browseButton', "Browse MCP Servers");
251
252
this._register(button.onDidClick(() => this.openerService.open(URI.parse(this.productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp'))));
253
}
254
255
private async query(query: string): Promise<IQueryResult> {
256
const disposables = new DisposableStore();
257
if (query) {
258
const servers = await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') });
259
return { model: new PagedModel(servers), disposables };
260
}
261
262
const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IWorkbenchMcpServer>>());
263
let servers = await this.mcpWorkbenchService.queryLocal();
264
disposables.add(Event.debounce(Event.filter(this.mcpWorkbenchService.onChange, e => e?.installState === McpServerInstallState.Installed), () => undefined)(() => {
265
const mergedMcpServers = this.mergeAddedMcpServers(servers, [...this.mcpWorkbenchService.local]);
266
if (mergedMcpServers) {
267
servers = mergedMcpServers;
268
onDidChangeModel.fire(new PagedModel(servers));
269
}
270
}));
271
disposables.add(this.mcpWorkbenchService.onReset(() => onDidChangeModel.fire(new PagedModel([...this.mcpWorkbenchService.local]))));
272
return { model: new PagedModel(servers), onDidChangeModel: onDidChangeModel.event, disposables };
273
}
274
275
private mergeAddedMcpServers(mcpServers: IWorkbenchMcpServer[], newMcpServers: IWorkbenchMcpServer[]): IWorkbenchMcpServer[] | undefined {
276
const oldMcpServers = [...mcpServers];
277
const findPreviousMcpServerIndex = (from: number): number => {
278
let index = -1;
279
const previousMcpServerInNew = newMcpServers[from];
280
if (previousMcpServerInNew) {
281
index = oldMcpServers.findIndex(e => e.id === previousMcpServerInNew.id);
282
if (index === -1) {
283
return findPreviousMcpServerIndex(from - 1);
284
}
285
}
286
return index;
287
};
288
289
let hasChanged: boolean = false;
290
for (let index = 0; index < newMcpServers.length; index++) {
291
const mcpServer = newMcpServers[index];
292
if (mcpServers.every(r => r.id !== mcpServer.id)) {
293
hasChanged = true;
294
mcpServers.splice(findPreviousMcpServerIndex(index - 1) + 1, 0, mcpServer);
295
}
296
}
297
298
return hasChanged ? mcpServers : undefined;
299
}
300
301
}
302
303
interface IMcpServerTemplateData {
304
root: HTMLElement;
305
element: HTMLElement;
306
name: HTMLElement;
307
description: HTMLElement;
308
starred: HTMLElement;
309
mcpServer: IWorkbenchMcpServer | null;
310
disposables: IDisposable[];
311
mcpServerDisposables: IDisposable[];
312
actionbar: ActionBar;
313
}
314
315
class McpServerRenderer implements IPagedRenderer<IWorkbenchMcpServer, IMcpServerTemplateData> {
316
317
static readonly templateId = 'mcpServer';
318
readonly templateId = McpServerRenderer.templateId;
319
320
constructor(
321
private readonly options: ExtensionListRendererOptions,
322
@IAllowedMcpServersService private readonly allowedMcpServersService: IAllowedMcpServersService,
323
@IInstantiationService private readonly instantiationService: IInstantiationService,
324
@INotificationService private readonly notificationService: INotificationService,
325
) { }
326
327
renderTemplate(root: HTMLElement): IMcpServerTemplateData {
328
const element = dom.append(root, dom.$('.mcp-server-item.extension-list-item'));
329
const iconContainer = dom.append(element, dom.$('.icon-container'));
330
const iconWidget = this.instantiationService.createInstance(McpServerIconWidget, iconContainer);
331
const details = dom.append(element, dom.$('.details'));
332
const headerContainer = dom.append(details, dom.$('.header-container'));
333
const header = dom.append(headerContainer, dom.$('.header'));
334
const name = dom.append(header, dom.$('span.name'));
335
const starred = dom.append(header, dom.$('span.ratings'));
336
const description = dom.append(details, dom.$('.description.ellipsis'));
337
const footer = dom.append(details, dom.$('.footer'));
338
const publisherWidget = this.instantiationService.createInstance(PublisherWidget, dom.append(footer, dom.$('.publisher-container')), true);
339
const actionbar = new ActionBar(footer, {
340
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
341
if (action instanceof DropDownAction) {
342
return action.createActionViewItem(options);
343
}
344
return undefined;
345
},
346
focusOnlyEnabledItems: true
347
});
348
349
actionbar.setFocusable(false);
350
const actionBarListener = actionbar.onDidRun(({ error }) => error && this.notificationService.error(error));
351
const mcpServerStatusAction = this.instantiationService.createInstance(McpServerStatusAction);
352
353
const actions = [
354
this.instantiationService.createInstance(InstallAction, false),
355
this.instantiationService.createInstance(InstallingLabelAction),
356
this.instantiationService.createInstance(ManageMcpServerAction, false),
357
mcpServerStatusAction
358
];
359
360
const widgets = [
361
iconWidget,
362
publisherWidget,
363
this.instantiationService.createInstance(StarredWidget, starred, true),
364
this.instantiationService.createInstance(McpServerScopeBadgeWidget, iconContainer),
365
this.instantiationService.createInstance(McpServerHoverWidget, { target: root, position: this.options.hoverOptions.position }, mcpServerStatusAction)
366
];
367
const extensionContainers: McpServerContainers = this.instantiationService.createInstance(McpServerContainers, [...actions, ...widgets]);
368
369
actionbar.push(actions, { icon: true, label: true });
370
const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers);
371
372
return {
373
root, element, name, description, starred, disposables: [disposable], actionbar,
374
mcpServerDisposables: [],
375
set mcpServer(mcpServer: IWorkbenchMcpServer) {
376
extensionContainers.mcpServer = mcpServer;
377
}
378
};
379
}
380
381
renderPlaceholder(index: number, data: IMcpServerTemplateData): void {
382
data.element.classList.add('loading');
383
384
data.mcpServerDisposables = dispose(data.mcpServerDisposables);
385
data.name.textContent = '';
386
data.description.textContent = '';
387
data.starred.style.display = 'none';
388
data.mcpServer = null;
389
}
390
391
renderElement(mcpServer: IWorkbenchMcpServer, index: number, data: IMcpServerTemplateData): void {
392
data.element.classList.remove('loading');
393
data.mcpServerDisposables = dispose(data.mcpServerDisposables);
394
data.root.setAttribute('data-mcp-server-id', mcpServer.id);
395
data.name.textContent = mcpServer.label;
396
data.description.textContent = mcpServer.description;
397
398
data.starred.style.display = '';
399
data.mcpServer = mcpServer;
400
401
const updateEnablement = () => {
402
const disabled = !!mcpServer.local &&
403
(mcpServer.installState === McpServerInstallState.Installed
404
? mcpServer.enablementState === McpServerEnablementState.DisabledByAccess
405
: mcpServer.installState === McpServerInstallState.Uninstalled);
406
data.root.classList.toggle('disabled', disabled);
407
};
408
updateEnablement();
409
this.allowedMcpServersService.onDidChangeAllowedMcpServers(() => updateEnablement(), this, data.mcpServerDisposables);
410
}
411
412
disposeElement(mcpServer: IWorkbenchMcpServer, index: number, data: IMcpServerTemplateData): void {
413
data.mcpServerDisposables = dispose(data.mcpServerDisposables);
414
}
415
416
disposeTemplate(data: IMcpServerTemplateData): void {
417
data.mcpServerDisposables = dispose(data.mcpServerDisposables);
418
data.disposables = dispose(data.disposables);
419
}
420
}
421
422
423
export class DefaultBrowseMcpServersView extends McpServersListView {
424
425
protected override renderBody(container: HTMLElement): void {
426
super.renderBody(container);
427
this._register(this.mcpGalleryManifestService.onDidChangeMcpGalleryManifest(() => this.show()));
428
}
429
430
override async show(): Promise<IPagedModel<IWorkbenchMcpServer>> {
431
return super.show('@mcp');
432
}
433
}
434
435
export class McpServersViewsContribution extends Disposable implements IWorkbenchContribution {
436
437
static ID = 'workbench.mcp.servers.views.contribution';
438
439
constructor() {
440
super();
441
442
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([
443
{
444
id: InstalledMcpServersViewId,
445
name: localize2('mcp-installed', "MCP Servers - Installed"),
446
ctorDescriptor: new SyncDescriptor(McpServersListView, [{ showWelcomeOnEmpty: false }]),
447
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext),
448
weight: 40,
449
order: 4,
450
canToggleVisibility: true
451
},
452
{
453
id: 'workbench.views.mcp.default.marketplace',
454
name: localize2('mcp', "MCP Servers"),
455
ctorDescriptor: new SyncDescriptor(DefaultBrowseMcpServersView, [{ showWelcomeOnEmpty: true }]),
456
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext.toNegated(), ChatContextKeys.Setup.hidden.negate()),
457
weight: 40,
458
order: 4,
459
canToggleVisibility: true
460
},
461
{
462
id: 'workbench.views.mcp.marketplace',
463
name: localize2('mcp', "MCP Servers"),
464
ctorDescriptor: new SyncDescriptor(McpServersListView, [{ showWelcomeOnEmpty: true }]),
465
when: ContextKeyExpr.and(SearchMcpServersContext),
466
}
467
], VIEW_CONTAINER);
468
}
469
}
470
471