diff --git a/src/vs/platform/mcp/common/mcpResourceScannerService.ts b/src/vs/platform/mcp/common/mcpResourceScannerService.ts index daa4f3774f4..5452abea713 100644 --- a/src/vs/platform/mcp/common/mcpResourceScannerService.ts +++ b/src/vs/platform/mcp/common/mcpResourceScannerService.ts @@ -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; @@ -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) { + (>config).type = (config).command ? 'stdio' : 'http'; + } scannedMcpServers.servers[serverName] = { id: serverName, name: serverName, diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 5d17e3b1f98..352c6fbb8ff 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -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(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); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 08dcfe1407e..6a306d92388 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -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({ diff --git a/src/vs/workbench/contrib/mcp/browser/mcpMigration.ts b/src/vs/workbench/contrib/mcp/browser/mcpMigration.ts index b70b71e112f..f9fe960e110 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpMigration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpMigration.ts @@ -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) { + (>config).type = (config).command ? 'stdio' : 'http'; + } + } + } + return mcpConfiguration; } catch (error) { this.logService.warn(`MCP migration: Failed to parse MCP config from ${settingsFile}:`, error); return; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index b07f7fdbc1b..6c10f225b1a 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -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 { 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 { + 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, {}); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 4fbfdb7ae02..b9c75e84f92 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -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 | 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> { + return super.show('@mcp'); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/media/mcpServerEditor.css b/src/vs/workbench/contrib/mcp/browser/media/mcpServerEditor.css new file mode 100644 index 00000000000..86aac122fbd --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/media/mcpServerEditor.css @@ -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; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index d7ccde3c119..8482ab14b75 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -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', diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index a7a4be5da5c..945667f6dba 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -629,7 +629,7 @@ export class McpServerContainers extends Disposable { } export const McpServersGalleryEnabledContext = new RawContextKey('mcpServersGalleryEnabled', false); -export const HasInstalledMcpServersContext = new RawContextKey('hasInstalledMcpServers', false); +export const HasInstalledMcpServersContext = new RawContextKey('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.'));