improve mcp server editor and view (#252291)

This commit is contained in:
Sandeep Somavarapu
2025-06-24 17:23:25 +02:00
committed by GitHub
parent 4fed93462a
commit 21e7d58551
9 changed files with 181 additions and 18 deletions
@@ -9,6 +9,7 @@ import { IStringDictionary } from '../../../base/common/collections.js';
import { parse, ParseError } from '../../../base/common/json.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../base/common/map.js';
import { Mutable } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { ConfigurationTarget, ConfigurationTargetToString } from '../../configuration/common/configuration.js';
import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
@@ -16,7 +17,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IScannedMcpServers, IScannedMcpServer } from './mcpManagement.js';
import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js';
import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration } from './mcpPlatformTypes.js';
interface IScannedWorkspaceFolderMcpServers {
servers?: IStringDictionary<IMcpServerConfiguration>;
@@ -188,6 +189,9 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc
if (servers.length > 0) {
scannedMcpServers.servers = {};
for (const [serverName, config] of servers) {
if (config.type === undefined) {
(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>config).command ? 'stdio' : 'http';
}
scannedMcpServers.servers[serverName] = {
id: serverName,
name: serverName,
@@ -34,7 +34,7 @@ import { McpSamplingService } from '../common/mcpSamplingService.js';
import { McpService } from '../common/mcpService.js';
import { HasInstalledMcpServersContext, IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId } from '../common/mcpTypes.js';
import { McpAddContextContribution } from './mcpAddContextContribution.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { McpDiscovery } from './mcpDiscovery.js';
import { McpElicitationService } from './mcpElicitationService.js';
import { McpLanguageFeatures } from './mcpLanguageFeatures.js';
@@ -42,7 +42,7 @@ import { McpConfigMigrationContribution } from './mcpMigration.js';
import { McpResourceQuickAccess } from './mcpResourceQuickAccess.js';
import { McpServerEditor } from './mcpServerEditor.js';
import { McpServerEditorInput } from './mcpServerEditorInput.js';
import { McpServersListView } from './mcpServersView.js';
import { DefaultBrowseMcpServersView, McpServersListView } from './mcpServersView.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchService.js';
@@ -78,6 +78,7 @@ registerAction2(InstallFromActivation);
registerAction2(RestartServer);
registerAction2(ShowConfiguration);
registerAction2(McpBrowseCommand);
registerAction2(ShowInstalledMcpServersCommand);
registerAction2(McpBrowseResourcesCommand);
registerAction2(McpConfigureSamplingModels);
registerAction2(McpStartPromptingServerCommand);
@@ -94,16 +95,25 @@ Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([
{
id: InstalledMcpServersViewId,
name: localize2('mcp-installed', "MCP Servers - Installed"),
ctorDescriptor: new SyncDescriptor(McpServersListView),
ctorDescriptor: new SyncDescriptor(McpServersListView, [{ showWelcomeOnEmpty: false }]),
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext),
weight: 40,
order: 4,
canToggleVisibility: true
},
{
id: 'workbench.views.mcp.default.marketplace',
name: localize2('mcp', "MCP Servers"),
ctorDescriptor: new SyncDescriptor(DefaultBrowseMcpServersView, [{ showWelcomeOnEmpty: true }]),
when: ContextKeyExpr.and(DefaultViewsContext, HasInstalledMcpServersContext.toNegated()),
weight: 40,
order: 4,
canToggleVisibility: true
},
{
id: 'workbench.views.mcp.marketplace',
name: localize2('mcp', "MCP Servers"),
ctorDescriptor: new SyncDescriptor(McpServersListView),
ctorDescriptor: new SyncDescriptor(McpServersListView, [{ showWelcomeOnEmpty: true }]),
when: ContextKeyExpr.and(SearchMcpServersContext),
}
], VIEW_CONTAINER);
@@ -18,11 +18,10 @@ import { SuggestController } from '../../../../editor/contrib/suggest/browser/su
import { ILocalizedString, localize, localize2 } from '../../../../nls.js';
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { Action2, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';
import { Action2, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { ExtensionsLocalizedLabel } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
@@ -46,7 +45,7 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { McpContextKeys } from '../common/mcpContextKeys.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, LazyCollectionState, McpCapability, McpConnectionState, mcpPromptPrefix, McpServerCacheState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpConnectionState, mcpPromptPrefix, McpServerCacheState } from '../common/mcpTypes.js';
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
@@ -763,11 +762,8 @@ export class McpBrowseCommand extends Action2 {
super({
id: McpCommandIds.Browse,
title: localize2('mcp.command.browse', "MCP Servers"),
category: ExtensionsLocalizedLabel,
category,
menu: [{
id: MenuId.CommandPalette,
when: McpServersGalleryEnabledContext,
}, {
id: extensionsFilterSubMenu,
group: '1_predefined',
order: 1,
@@ -780,6 +776,30 @@ export class McpBrowseCommand extends Action2 {
}
}
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: McpCommandIds.Browse,
title: localize2('mcp.command.browse.mcp', "Browse Servers"),
category
},
});
export class ShowInstalledMcpServersCommand extends Action2 {
constructor() {
super({
id: McpCommandIds.ShowInstalled,
title: localize2('mcp.command.show.installed', "Show Installed Servers"),
category,
precondition: HasInstalledMcpServersContext,
f1: true,
});
}
async run(accessor: ServicesAccessor) {
accessor.get(IViewsService).openView(InstalledMcpServersViewId, true);
}
}
export class McpBrowseResourcesCommand extends Action2 {
constructor() {
super({
@@ -6,7 +6,7 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IMcpServerConfiguration, IMcpServerVariable } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
import { IStringDictionary } from '../../../../base/common/collections.js';
import { mcpConfigurationSection } from '../../../contrib/mcp/common/mcpConfiguration.js';
import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
@@ -15,7 +15,7 @@ import { IUserDataProfileService } from '../../../services/userDataProfile/commo
import { IFileService } from '../../../../platform/files/common/files.js';
import { URI } from '../../../../base/common/uri.js';
import { parse } from '../../../../base/common/jsonc.js';
import { isObject } from '../../../../base/common/types.js';
import { isObject, Mutable } from '../../../../base/common/types.js';
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';
@@ -73,7 +73,15 @@ export class McpConfigMigrationContribution extends Disposable implements IWorkb
if (!isObject(settingsObject)) {
return undefined;
}
return settingsObject[mcpConfigurationSection] as IMcpConfiguration;
const mcpConfiguration = settingsObject[mcpConfigurationSection] as IMcpConfiguration;
if (mcpConfiguration && mcpConfiguration.servers) {
for (const [, config] of Object.entries(mcpConfiguration.servers)) {
if (config.type === undefined) {
(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>config).command ? 'stdio' : 'http';
}
}
}
return mcpConfiguration;
} catch (error) {
this.logService.warn(`MCP migration: Failed to parse MCP config from ${settingsFile}:`, error);
return;
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/mcpServerEditor.css';
import { $, Dimension, append, setParentFlowTo } from '../../../../base/browser/dom.js';
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
@@ -46,6 +47,7 @@ import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/ac
const enum McpServerEditorTab {
Readme = 'readme',
Configuration = 'configuration',
}
function toDateString(date: Date) {
@@ -172,7 +174,7 @@ export class McpServerEditor extends EditorPane {
}
protected createEditor(parent: HTMLElement): void {
const root = append(parent, $('.extension-editor'));
const root = append(parent, $('.extension-editor.mcp-server-editor'));
this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);
this._scopedContextKeyService.value.createKey('inExtensionEditor', true);
@@ -316,6 +318,11 @@ export class McpServerEditor extends EditorPane {
}
template.navbar.push(McpServerEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file"));
if (extension.local) {
template.navbar.push(McpServerEditorTab.Configuration, localize('configuration', "Configuration"), localize('configurationtooltip', "Server configuration details"));
}
if (template.navbar.currentId) {
this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template);
}
@@ -371,6 +378,7 @@ export class McpServerEditor extends EditorPane {
private open(id: string, extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
switch (id) {
case McpServerEditorTab.Configuration: return this.openConfiguration(extension, template, token);
case McpServerEditorTab.Readme: return this.openDetails(extension, template, token);
}
return Promise.resolve(null);
@@ -536,6 +544,73 @@ export class McpServerEditor extends EditorPane {
return activeElement;
}
private async openConfiguration(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
const configContainer = append(template.content, $('.configuration'));
const content = $('div', { class: 'configuration-content', tabindex: '0' });
this.renderConfigurationDetails(content, mcpServer);
const scrollableContent = new DomScrollableElement(content, {});
const layout = () => scrollableContent.scanDomNode();
this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));
append(configContainer, scrollableContent.getDomNode());
return { focus: () => content.focus() };
}
private renderConfigurationDetails(container: HTMLElement, mcpServer: IWorkbenchMcpServer): void {
container.remove();
if (!mcpServer.local) {
const noConfigMessage = append(container, $('.no-config'));
noConfigMessage.textContent = localize('noConfig', "No configuration available for this MCP server.");
return;
}
const config = mcpServer.local.config;
// Server Name
const nameSection = append(container, $('.config-section'));
const nameLabel = append(nameSection, $('.config-label'));
nameLabel.textContent = localize('serverName', "Name:");
const nameValue = append(nameSection, $('.config-value'));
nameValue.textContent = mcpServer.local.name;
// Server Type
const typeSection = append(container, $('.config-section'));
const typeLabel = append(typeSection, $('.config-label'));
typeLabel.textContent = localize('serverType', "Type:");
const typeValue = append(typeSection, $('.config-value'));
typeValue.textContent = config.type;
// Type-specific configuration
if (config.type === 'stdio') {
// Command
const commandSection = append(container, $('.config-section'));
const commandLabel = append(commandSection, $('.config-label'));
commandLabel.textContent = localize('command', "Command:");
const commandValue = append(commandSection, $('code.config-value'));
commandValue.textContent = config.command;
// Arguments (if present)
if (config.args && config.args.length > 0) {
const argsSection = append(container, $('.config-section'));
const argsLabel = append(argsSection, $('.config-label'));
argsLabel.textContent = localize('arguments', "Arguments:");
const argsValue = append(argsSection, $('code.config-value'));
argsValue.textContent = config.args.join(' ');
}
} else if (config.type === 'http') {
// URL
const urlSection = append(container, $('.config-section'));
const urlLabel = append(urlSection, $('.config-label'));
urlLabel.textContent = localize('url', "URL:");
const urlValue = append(urlSection, $('code.config-value'));
urlValue.textContent = config.url;
}
}
private renderAdditionalDetails(container: HTMLElement, extension: IWorkbenchMcpServer): void {
const content = $('div', { class: 'additional-details-content', tabindex: '0' });
const scrollableContent = new DomScrollableElement(content, {});
@@ -36,6 +36,10 @@ import { URI } from '../../../../base/common/uri.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
export interface McpServerListViewOptions {
showWelcomeOnEmpty?: boolean;
}
export class McpServersListView extends ViewPane {
private list: WorkbenchPagedList<IWorkbenchMcpServer> | null = null;
@@ -44,6 +48,7 @@ export class McpServersListView extends ViewPane {
private readonly contextMenuActionRunner = this._register(new ActionRunner());
constructor(
private readonly mpcViewOptions: McpServerListViewOptions,
options: IViewletViewOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@@ -143,7 +148,7 @@ export class McpServersListView extends ViewPane {
const servers = query ? await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }) : await this.mcpWorkbenchService.queryLocal();
this.list.model = new DelayedPagedModel(new PagedModel(servers));
this.showWelcomeContent(!this.mcpGalleryService.isEnabled() && servers.length === 0);
this.showWelcomeContent(!this.mcpGalleryService.isEnabled() && servers.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty);
return this.list.model;
}
@@ -273,3 +278,10 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
data.disposables = dispose(data.disposables);
}
}
export class DefaultBrowseMcpServersView extends McpServersListView {
override async show(): Promise<IPagedModel<IWorkbenchMcpServer>> {
return super.show('@mcp');
}
}
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.mcp-server-editor {
.configuration-content {
padding: 20px;
box-sizing: border-box;
}
.config-section {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.config-label {
font-weight: 600;
min-width: 80px;
margin-right: 15px;
}
.config-value {
word-break: break-all;
}
.no-config {
color: var(--vscode-descriptionForeground);
font-style: italic;
padding: 20px;
}
}
@@ -9,6 +9,7 @@
export const enum McpCommandIds {
AddConfiguration = 'workbench.mcp.addConfiguration',
Browse = 'workbench.mcp.browseServers',
ShowInstalled = 'workbench.mcp.showInstalledServers',
BrowseResources = 'workbench.mcp.browseResources',
ConfigureSamplingModels = 'workbench.mcp.configureSamplingModels',
EditStoredInput = 'workbench.mcp.editStoredInput',
@@ -629,7 +629,7 @@ export class McpServerContainers extends Disposable {
}
export const McpServersGalleryEnabledContext = new RawContextKey<boolean>('mcpServersGalleryEnabled', false);
export const HasInstalledMcpServersContext = new RawContextKey<boolean>('hasInstalledMcpServers', false);
export const HasInstalledMcpServersContext = new RawContextKey<boolean>('hasInstalledMcpServers', true);
export const InstalledMcpServersViewId = 'workbench.views.mcp.installed';
export const mcpServerIcon = registerIcon('mcp-server', Codicon.mcp, localize('mcpServer', 'Icon used for the MCP server.'));