Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $, addDisposableListener, disposableWindowInterval, EventType, h } from '../../../../base/browser/dom.js';6import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';7import { IManagedHoverTooltipHTMLElement } from '../../../../base/browser/ui/hover/hover.js';8import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js';9import { mainWindow } from '../../../../base/browser/window.js';10import { findLast } from '../../../../base/common/arraysFind.js';11import { assertNever } from '../../../../base/common/assert.js';12import { VSBuffer } from '../../../../base/common/buffer.js';13import { Codicon } from '../../../../base/common/codicons.js';14import { groupBy } from '../../../../base/common/collections.js';15import { Event } from '../../../../base/common/event.js';16import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js';17import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';18import { autorun, derived, derivedObservableWithCache, observableValue } from '../../../../base/common/observable.js';19import { ThemeIcon } from '../../../../base/common/themables.js';20import { isDefined } from '../../../../base/common/types.js';21import { URI } from '../../../../base/common/uri.js';22import { Range } from '../../../../editor/common/core/range.js';23import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';24import { ILocalizedString, localize, localize2 } from '../../../../nls.js';25import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';26import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';27import { Action2, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js';28import { ICommandService } from '../../../../platform/commands/common/commands.js';29import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';30import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';31import { IFileService } from '../../../../platform/files/common/files.js';32import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';33import { McpGalleryManifestStatus } from '../../../../platform/mcp/common/mcpGalleryManifest.js';34import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';35import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';36import { IOpenerService } from '../../../../platform/opener/common/opener.js';37import { IProductService } from '../../../../platform/product/common/productService.js';38import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';39import { StorageScope } from '../../../../platform/storage/common/storage.js';40import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';41import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js';42import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';43import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';44import { ActiveEditorContext, RemoteNameContext, ResourceContextKey, WorkbenchStateContext, WorkspaceFolderCountContext } from '../../../common/contextkeys.js';45import { IWorkbenchContribution } from '../../../common/contributions.js';46import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';47import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';48import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';49import { IEditorService } from '../../../services/editor/common/editorService.js';50import { IRemoteUserDataProfilesService } from '../../../services/userDataProfile/common/remoteUserDataProfiles.js';51import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';52import { IViewsService } from '../../../services/views/common/viewsService.js';53import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js';54import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js';55import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';56import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService.js';57import { ChatModeKind } from '../../chat/common/constants.js';58import { ILanguageModelsService } from '../../chat/common/languageModels.js';59import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js';60import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';61import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';62import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';63import { McpCommandIds } from '../common/mcpCommandIds.js';64import { McpContextKeys } from '../common/mcpContextKeys.js';65import { IMcpRegistry } from '../common/mcpRegistryTypes.js';66import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpServersGalleryStatusContext, McpStartServerInteraction } from '../common/mcpTypes.js';67import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';68import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';69import './media/mcpServerAction.css';70import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';7172// acroynms do not get localized73const category: ILocalizedString = {74original: 'MCP',75value: 'MCP',76};7778export class ListMcpServerCommand extends Action2 {79constructor() {80super({81id: McpCommandIds.ListServer,82title: localize2('mcp.list', 'List Servers'),83icon: Codicon.server,84category,85f1: true,86menu: [{87when: ContextKeyExpr.and(88ContextKeyExpr.or(89ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools),90McpContextKeys.hasServersWithErrors,91),92ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),93ChatContextKeys.lockedToCodingAgent.negate()94),95id: MenuId.ChatExecute,96group: 'navigation',97order: 2,98}],99});100}101102override async run(accessor: ServicesAccessor) {103const mcpService = accessor.get(IMcpService);104const commandService = accessor.get(ICommandService);105const quickInput = accessor.get(IQuickInputService);106107type ItemType = { id: string } & IQuickPickItem;108109const store = new DisposableStore();110const pick = quickInput.createQuickPick<ItemType>({ useSeparators: true });111pick.placeholder = localize('mcp.selectServer', 'Select an MCP Server');112113store.add(pick);114115store.add(autorun(reader => {116const servers = groupBy(mcpService.servers.read(reader).slice().sort((a, b) => (a.collection.presentation?.order || 0) - (b.collection.presentation?.order || 0)), s => s.collection.id);117const firstRun = pick.items.length === 0;118pick.items = [119{ id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) },120...Object.values(servers).filter(s => s.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [121{ type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id },122...servers.map(server => ({123id: server.definition.id,124label: server.definition.label,125description: McpConnectionState.toString(server.connectionState.read(reader)),126})),127]),128];129130if (firstRun && pick.items.length > 3) {131pick.activeItems = pick.items.slice(2, 3) as ItemType[]; // select the first server by default132}133}));134135136const picked = await new Promise<ItemType | undefined>(resolve => {137store.add(pick.onDidAccept(() => {138resolve(pick.activeItems[0]);139}));140store.add(pick.onDidHide(() => {141resolve(undefined);142}));143pick.show();144});145146store.dispose();147148if (!picked) {149// no-op150} else if (picked.id === '$add') {151commandService.executeCommand(McpCommandIds.AddConfiguration);152} else {153commandService.executeCommand(McpCommandIds.ServerOptions, picked.id);154}155}156}157158interface ActionItem extends IQuickPickItem {159action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config' | 'configSampling' | 'samplingLog' | 'resources';160}161162interface AuthActionItem extends IQuickPickItem {163action: 'disconnect' | 'signout';164accountQuery: IAccountQuery;165}166167export class McpConfirmationServerOptionsCommand extends Action2 {168constructor() {169super({170id: McpCommandIds.ServerOptionsInConfirmation,171title: localize2('mcp.options', 'Server Options'),172category,173icon: Codicon.settingsGear,174f1: false,175menu: [{176id: MenuId.ChatConfirmationMenu,177when: ContextKeyExpr.and(178ContextKeyExpr.equals('chatConfirmationPartSource', 'mcp'),179ContextKeyExpr.or(180ContextKeyExpr.equals('chatConfirmationPartType', 'chatToolConfirmation'),181ContextKeyExpr.equals('chatConfirmationPartType', 'elicitation'),182),183),184group: 'navigation'185}],186});187}188189override async run(accessor: ServicesAccessor, arg: IChatToolInvocation | IChatElicitationRequest): Promise<void> {190const toolsService = accessor.get(ILanguageModelToolsService);191if (arg.kind === 'toolInvocation') {192const tool = toolsService.getTool(arg.toolId);193if (tool?.source.type === 'mcp') {194accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, tool.source.definitionId);195}196} else if (arg.kind === 'elicitation') {197if (arg.source?.type === 'mcp') {198accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, arg.source.definitionId);199}200} else {201assertNever(arg);202}203}204}205206export class McpServerOptionsCommand extends Action2 {207constructor() {208super({209id: McpCommandIds.ServerOptions,210title: localize2('mcp.options', 'Server Options'),211category,212f1: false,213});214}215216override async run(accessor: ServicesAccessor, id: string): Promise<void> {217const mcpService = accessor.get(IMcpService);218const quickInputService = accessor.get(IQuickInputService);219const mcpRegistry = accessor.get(IMcpRegistry);220const editorService = accessor.get(IEditorService);221const commandService = accessor.get(ICommandService);222const samplingService = accessor.get(IMcpSamplingService);223const authenticationQueryService = accessor.get(IAuthenticationQueryService);224const authenticationService = accessor.get(IAuthenticationService);225const server = mcpService.servers.get().find(s => s.definition.id === id);226if (!server) {227return;228}229230const collection = mcpRegistry.collections.get().find(c => c.id === server.collection.id);231const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);232233const items: (ActionItem | AuthActionItem | IQuickPickSeparator)[] = [];234const serverState = server.connectionState.get();235236items.push({ type: 'separator', label: localize('mcp.actions.status', 'Status') });237238// Only show start when server is stopped or in error state239if (McpConnectionState.canBeStarted(serverState.state)) {240items.push({241label: localize('mcp.start', 'Start Server'),242action: 'start'243});244} else {245items.push({246label: localize('mcp.stop', 'Stop Server'),247action: 'stop'248});249items.push({250label: localize('mcp.restart', 'Restart Server'),251action: 'restart'252});253}254255items.push(...this._getAuthActions(authenticationQueryService, server.definition.id));256257const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;258if (configTarget) {259items.push({260label: localize('mcp.config', 'Show Configuration'),261action: 'config',262});263}264265items.push({266label: localize('mcp.showOutput', 'Show Output'),267action: 'showOutput'268});269270items.push(271{ type: 'separator', label: localize('mcp.actions.sampling', 'Sampling') },272{273label: localize('mcp.configAccess', 'Configure Model Access'),274description: localize('mcp.showOutput.description', 'Set the models the server can use via MCP sampling'),275action: 'configSampling'276},277);278279280if (samplingService.hasLogs(server)) {281items.push({282label: localize('mcp.samplingLog', 'Show Sampling Requests'),283description: localize('mcp.samplingLog.description', 'Show the sampling requests for this server'),284action: 'samplingLog',285});286}287288const capabilities = server.capabilities.get();289if (capabilities === undefined || (capabilities & McpCapability.Resources)) {290items.push({ type: 'separator', label: localize('mcp.actions.resources', 'Resources') });291items.push({292label: localize('mcp.resources', 'Browse Resources'),293action: 'resources',294});295}296297const pick = await quickInputService.pick(items, {298placeHolder: localize('mcp.selectAction', 'Select action for \'{0}\'', server.definition.label),299});300301if (!pick) {302return;303}304305switch (pick.action) {306case 'start':307await server.start({ promptType: 'all-untrusted' });308server.showOutput();309break;310case 'stop':311await server.stop();312break;313case 'restart':314await server.stop();315await server.start({ promptType: 'all-untrusted' });316break;317case 'disconnect':318await server.stop();319await this._handleAuth(authenticationService, pick.accountQuery, server.definition, false);320break;321case 'signout':322await server.stop();323await this._handleAuth(authenticationService, pick.accountQuery, server.definition, true);324break;325case 'showOutput':326server.showOutput();327break;328case 'config':329editorService.openEditor({330resource: URI.isUri(configTarget) ? configTarget : configTarget!.uri,331options: { selection: URI.isUri(configTarget) ? undefined : configTarget!.range }332});333break;334case 'configSampling':335return commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);336case 'resources':337return commandService.executeCommand(McpCommandIds.BrowseResources, server);338case 'samplingLog':339editorService.openEditor({340resource: undefined,341contents: samplingService.getLogText(server),342label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),343});344break;345default:346assertNever(pick);347}348}349350private _getAuthActions(351authenticationQueryService: IAuthenticationQueryService,352serverId: string353): AuthActionItem[] {354const result: AuthActionItem[] = [];355// Really, this should only ever have one entry.356for (const [providerId, accountName] of authenticationQueryService.mcpServer(serverId).getAllAccountPreferences()) {357358const accountQuery = authenticationQueryService.provider(providerId).account(accountName);359if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {360continue; // skip accounts that are not allowed361}362// If there are multiple allowed servers/extensions, other things are using this provider363// so we show a disconnect action, otherwise we show a sign out action.364if (accountQuery.entities().getEntityCount().total > 1) {365result.push({366action: 'disconnect',367label: localize('mcp.disconnect', 'Disconnect Account'),368description: `(${accountName})`,369accountQuery370});371} else {372result.push({373action: 'signout',374label: localize('mcp.signOut', 'Sign Out'),375description: `(${accountName})`,376accountQuery377});378}379}380return result;381}382383private async _handleAuth(384authenticationService: IAuthenticationService,385accountQuery: IAccountQuery,386definition: McpDefinitionReference,387signOut: boolean388) {389const { providerId, accountName } = accountQuery;390accountQuery.mcpServer(definition.id).setAccessAllowed(false, definition.label);391if (signOut) {392const accounts = await authenticationService.getAccounts(providerId);393const account = accounts.find(a => a.label === accountName);394if (account) {395const sessions = await authenticationService.getSessions(providerId, undefined, { account });396for (const session of sessions) {397await authenticationService.removeSession(providerId, session.id);398}399}400}401}402}403404export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution {405constructor(406@IActionViewItemService actionViewItemService: IActionViewItemService,407@IMcpService mcpService: IMcpService,408@IInstantiationService instaService: IInstantiationService,409@ICommandService commandService: ICommandService,410@IConfigurationService configurationService: IConfigurationService,411) {412super();413414const hoverIsOpen = observableValue(this, false);415const config = observableConfigValue(mcpAutoStartConfig, McpAutoStartValue.NewAndOutdated, configurationService);416417const enum DisplayedState {418None,419NewTools,420Error,421Refreshing,422}423424type DisplayedStateT = {425state: DisplayedState;426servers: (IMcpServer | McpCollectionDefinition)[];427};428429function isServer(s: IMcpServer | McpCollectionDefinition): s is IMcpServer {430return typeof (s as IMcpServer).start === 'function';431}432433const displayedStateCurrent = derived((reader): DisplayedStateT => {434const servers = mcpService.servers.read(reader);435const serversPerState: (IMcpServer | McpCollectionDefinition)[][] = [];436for (const server of servers) {437let thisState = DisplayedState.None;438switch (server.cacheState.read(reader)) {439case McpServerCacheState.Unknown:440case McpServerCacheState.Outdated:441thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools;442break;443case McpServerCacheState.RefreshingFromUnknown:444thisState = DisplayedState.Refreshing;445break;446default:447thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None;448break;449}450451serversPerState[thisState] ??= [];452serversPerState[thisState].push(server);453}454455const unknownServerStates = mcpService.lazyCollectionState.read(reader);456if (unknownServerStates.state === LazyCollectionState.LoadingUnknown) {457serversPerState[DisplayedState.Refreshing] ??= [];458serversPerState[DisplayedState.Refreshing].push(...unknownServerStates.collections);459} else if (unknownServerStates.state === LazyCollectionState.HasUnknown) {460serversPerState[DisplayedState.NewTools] ??= [];461serversPerState[DisplayedState.NewTools].push(...unknownServerStates.collections);462}463464let maxState = (serversPerState.length - 1) as DisplayedState;465if (maxState === DisplayedState.NewTools && config.read(reader) !== McpAutoStartValue.Never) {466maxState = DisplayedState.None;467}468469return { state: maxState, servers: serversPerState[maxState] || [] };470});471472// avoid hiding the hover if a state changes while it's open:473const displayedState = derivedObservableWithCache<DisplayedStateT>(this, (reader, last) => {474if (last && hoverIsOpen.read(reader)) {475return last;476} else {477return displayedStateCurrent.read(reader);478}479});480481this._store.add(actionViewItemService.register(MenuId.ChatExecute, McpCommandIds.ListServer, (action, options) => {482if (!(action instanceof MenuItemAction)) {483return undefined;484}485486return instaService.createInstance(class extends MenuEntryActionViewItem {487488override render(container: HTMLElement): void {489490super.render(container);491container.classList.add('chat-mcp');492493const action = h('button.chat-mcp-action', [h('span@icon')]);494495this._register(autorun(r => {496const displayed = displayedState.read(r);497const { state } = displayed;498const { root, icon } = action;499this.updateTooltip();500container.classList.toggle('chat-mcp-has-action', state !== DisplayedState.None);501502if (!root.parentElement) {503container.appendChild(root);504}505506root.ariaLabel = this.getLabelForState(displayed);507root.className = 'chat-mcp-action';508icon.className = '';509if (state === DisplayedState.NewTools) {510root.classList.add('chat-mcp-action-new');511icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.refresh));512} else if (state === DisplayedState.Error) {513root.classList.add('chat-mcp-action-error');514icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning));515} else if (state === DisplayedState.Refreshing) {516root.classList.add('chat-mcp-action-refreshing');517icon.classList.add(...ThemeIcon.asClassNameArray(spinningLoading));518} else {519root.remove();520}521}));522}523524override async onClick(e: MouseEvent): Promise<void> {525e.preventDefault();526e.stopPropagation();527528const { state, servers } = displayedStateCurrent.get();529if (state === DisplayedState.NewTools) {530const interaction = new McpStartServerInteraction();531servers.filter(isServer).forEach(server => server.stop().then(() => server.start({ interaction })));532mcpService.activateCollections();533} else if (state === DisplayedState.Refreshing) {534findLast(servers, isServer)?.showOutput();535} else if (state === DisplayedState.Error) {536const server = findLast(servers, isServer);537if (server) {538await server.showOutput(true);539commandService.executeCommand(McpCommandIds.ServerOptions, server.definition.id);540}541} else {542commandService.executeCommand(McpCommandIds.ListServer);543}544}545546protected override getTooltip(): string {547return this.getLabelForState() || super.getTooltip();548}549550protected override getHoverContents({ state, servers } = displayedStateCurrent.get()): string | undefined | IManagedHoverTooltipHTMLElement {551const link = (s: IMcpServer) => markdownCommandLink({552title: s.definition.label,553id: McpCommandIds.ServerOptions,554arguments: [s.definition.id],555});556557const single = servers.length === 1;558const names = servers.map(s => isServer(s) ? link(s) : '`' + s.label + '`').map(l => single ? l : `- ${l}`).join('\n');559let markdown: MarkdownString;560if (state === DisplayedState.NewTools) {561markdown = new MarkdownString(single562? localize('mcp.newTools.md.single', "MCP server {0} has been updated and may have new tools available.", names)563: localize('mcp.newTools.md.multi', "MCP servers have been updated and may have new tools available:\n\n{0}", names)564);565} else if (state === DisplayedState.Error) {566markdown = new MarkdownString(single567? localize('mcp.err.md.single', "MCP server {0} was unable to start successfully.", names)568: localize('mcp.err.md.multi', "Multiple MCP servers were unable to start successfully:\n\n{0}", names)569);570} else {571return this.getLabelForState() || undefined;572}573574return {575element: (token): HTMLElement => {576hoverIsOpen.set(true, undefined);577578const store = new DisposableStore();579store.add(toDisposable(() => hoverIsOpen.set(false, undefined)));580store.add(token.onCancellationRequested(() => {581store.dispose();582}));583584// todo@connor4312/@benibenj: workaround for #257923585store.add(disposableWindowInterval(mainWindow, () => {586if (!container.isConnected) {587store.dispose();588}589}, 2000));590591const container = $('div.mcp-hover-contents');592593// Render markdown content594markdown.isTrusted = true;595const markdownResult = store.add(renderMarkdown(markdown));596container.appendChild(markdownResult.element);597598// Add divider599const divider = $('hr.mcp-hover-divider');600container.appendChild(divider);601602// Add checkbox for mcpAutoStartConfig setting603const checkboxContainer = $('div.mcp-hover-setting');604const settingLabelStr = localize('mcp.autoStart', "Automatically start MCP servers when sending a chat message");605606const checkbox = store.add(new Checkbox(607settingLabelStr,608config.get() !== McpAutoStartValue.Never,609defaultCheckboxStyles610));611612checkboxContainer.appendChild(checkbox.domNode);613614// Add label next to checkbox615const settingLabel = $('span.mcp-hover-setting-label', undefined, settingLabelStr);616checkboxContainer.appendChild(settingLabel);617618const onChange = () => {619const newValue = checkbox.checked ? McpAutoStartValue.NewAndOutdated : McpAutoStartValue.Never;620configurationService.updateValue(mcpAutoStartConfig, newValue);621};622623store.add(checkbox.onChange(onChange));624625store.add(addDisposableListener(settingLabel, EventType.CLICK, () => {626checkbox.checked = !checkbox.checked;627onChange();628}));629container.appendChild(checkboxContainer);630631return container;632},633};634}635636private getLabelForState({ state, servers } = displayedStateCurrent.get()) {637if (state === DisplayedState.NewTools) {638return localize('mcp.newTools', "New tools available ({0})", servers.length || 1);639} else if (state === DisplayedState.Error) {640return localize('mcp.toolError', "Error loading {0} tool(s)", servers.length || 1);641} else if (state === DisplayedState.Refreshing) {642return localize('mcp.toolRefresh', "Discovering tools...");643} else {644return null;645}646}647}, action, { ...options, keybindingNotRenderedWithLabel: true });648649}, Event.fromObservable(displayedState)));650}651}652653export class ResetMcpTrustCommand extends Action2 {654constructor() {655super({656id: McpCommandIds.ResetTrust,657title: localize2('mcp.resetTrust', "Reset Trust"),658category,659f1: true,660precondition: McpContextKeys.toolsCount.greater(0),661});662}663664run(accessor: ServicesAccessor): void {665const mcpService = accessor.get(IMcpService);666mcpService.resetTrust();667}668}669670671export class ResetMcpCachedTools extends Action2 {672constructor() {673super({674id: McpCommandIds.ResetCachedTools,675title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),676category,677f1: true,678precondition: McpContextKeys.toolsCount.greater(0),679});680}681682run(accessor: ServicesAccessor): void {683const mcpService = accessor.get(IMcpService);684mcpService.resetCaches();685}686}687688export class AddConfigurationAction extends Action2 {689constructor() {690super({691id: McpCommandIds.AddConfiguration,692title: localize2('mcp.addConfiguration', "Add Server..."),693metadata: {694description: localize2('mcp.addConfiguration.description', "Installs a new Model Context protocol to the mcp.json settings"),695},696category,697f1: true,698menu: {699id: MenuId.EditorContent,700when: ContextKeyExpr.and(701ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/),702ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID)703)704}705});706}707708async run(accessor: ServicesAccessor, configUri?: string): Promise<void> {709const instantiationService = accessor.get(IInstantiationService);710const workspaceService = accessor.get(IWorkspaceContextService);711const target = configUri ? workspaceService.getWorkspaceFolder(URI.parse(configUri)) : undefined;712return instantiationService.createInstance(McpAddConfigurationCommand, target ?? undefined).run();713}714}715716717export class RemoveStoredInput extends Action2 {718constructor() {719super({720id: McpCommandIds.RemoveStoredInput,721title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),722category,723f1: false,724});725}726727run(accessor: ServicesAccessor, scope: StorageScope, id?: string): void {728accessor.get(IMcpRegistry).clearSavedInputs(scope, id);729}730}731732export class EditStoredInput extends Action2 {733constructor() {734super({735id: McpCommandIds.EditStoredInput,736title: localize2('mcp.editStoredInput', "Edit Stored Input"),737category,738f1: false,739});740}741742run(accessor: ServicesAccessor, inputId: string, uri: URI | undefined, configSection: string, target: ConfigurationTarget): void {743const workspaceFolder = uri && accessor.get(IWorkspaceContextService).getWorkspaceFolder(uri);744accessor.get(IMcpRegistry).editSavedInput(inputId, workspaceFolder || undefined, configSection, target);745}746}747748export class ShowConfiguration extends Action2 {749constructor() {750super({751id: McpCommandIds.ShowConfiguration,752title: localize2('mcp.command.showConfiguration', "Show Configuration"),753category,754f1: false,755});756}757758run(accessor: ServicesAccessor, collectionId: string, serverId: string): void {759const collection = accessor.get(IMcpRegistry).collections.get().find(c => c.id === collectionId);760if (!collection) {761return;762}763764const server = collection?.serverDefinitions.get().find(s => s.id === serverId);765const editorService = accessor.get(IEditorService);766if (server?.presentation?.origin) {767editorService.openEditor({768resource: server.presentation.origin.uri,769options: { selection: server.presentation.origin.range }770});771} else if (collection.presentation?.origin) {772editorService.openEditor({773resource: collection.presentation.origin,774});775}776}777}778779export class ShowOutput extends Action2 {780constructor() {781super({782id: McpCommandIds.ShowOutput,783title: localize2('mcp.command.showOutput', "Show Output"),784category,785f1: false,786});787}788789run(accessor: ServicesAccessor, serverId: string): void {790accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId)?.showOutput();791}792}793794export class RestartServer extends Action2 {795constructor() {796super({797id: McpCommandIds.RestartServer,798title: localize2('mcp.command.restartServer', "Restart Server"),799category,800f1: false,801});802}803804async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {805const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);806s?.showOutput();807await s?.stop();808await s?.start({ promptType: 'all-untrusted', ...opts });809}810}811812export class StartServer extends Action2 {813constructor() {814super({815id: McpCommandIds.StartServer,816title: localize2('mcp.command.startServer', "Start Server"),817category,818f1: false,819});820}821822async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {823const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);824await s?.start({ promptType: 'all-untrusted', ...opts });825}826}827828export class StopServer extends Action2 {829constructor() {830super({831id: McpCommandIds.StopServer,832title: localize2('mcp.command.stopServer', "Stop Server"),833category,834f1: false,835});836}837838async run(accessor: ServicesAccessor, serverId: string) {839const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);840await s?.stop();841}842}843844export class McpBrowseCommand extends Action2 {845constructor() {846super({847id: McpCommandIds.Browse,848title: localize2('mcp.command.browse', "MCP Servers"),849category,850menu: [{851id: extensionsFilterSubMenu,852group: '1_predefined',853order: 1,854}],855});856}857858async run(accessor: ServicesAccessor) {859accessor.get(IExtensionsWorkbenchService).openSearch('@mcp ');860}861}862863MenuRegistry.appendMenuItem(MenuId.CommandPalette, {864command: {865id: McpCommandIds.Browse,866title: localize2('mcp.command.browse.mcp', "Browse Servers"),867category868},869});870871export class BrowseMcpServersPageCommand extends Action2 {872constructor() {873super({874id: McpCommandIds.BrowsePage,875title: localize2('mcp.command.open', "Browse MCP Servers"),876icon: Codicon.globe,877menu: [{878id: MenuId.ViewTitle,879when: ContextKeyExpr.and(ContextKeyExpr.equals('view', InstalledMcpServersViewId), McpServersGalleryStatusContext.isEqualTo(McpGalleryManifestStatus.Unavailable)),880group: 'navigation',881}],882});883}884885async run(accessor: ServicesAccessor) {886const productService = accessor.get(IProductService);887const openerService = accessor.get(IOpenerService);888return openerService.open(productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp');889}890}891892export class ShowInstalledMcpServersCommand extends Action2 {893constructor() {894super({895id: McpCommandIds.ShowInstalled,896title: localize2('mcp.command.show.installed', "Show Installed Servers"),897category,898precondition: HasInstalledMcpServersContext,899f1: true,900});901}902903async run(accessor: ServicesAccessor) {904const viewsService = accessor.get(IViewsService);905const view = await viewsService.openView(InstalledMcpServersViewId, true);906if (!view) {907await viewsService.openViewContainer(VIEW_CONTAINER.id);908await viewsService.openView(InstalledMcpServersViewId, true);909}910}911}912913MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, {914command: {915id: McpCommandIds.ShowInstalled,916title: localize2('mcp.servers', "MCP Servers")917},918when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),919order: 14,920group: '0_level'921});922923abstract class OpenMcpResourceCommand extends Action2 {924protected abstract getURI(accessor: ServicesAccessor): Promise<URI>;925926async run(accessor: ServicesAccessor) {927const fileService = accessor.get(IFileService);928const editorService = accessor.get(IEditorService);929const resource = await this.getURI(accessor);930if (!(await fileService.exists(resource))) {931await fileService.createFile(resource, VSBuffer.fromString(JSON.stringify({ servers: {} }, null, '\t')));932}933await editorService.openEditor({ resource });934}935}936937export class OpenUserMcpResourceCommand extends OpenMcpResourceCommand {938constructor() {939super({940id: McpCommandIds.OpenUserMcp,941title: localize2('mcp.command.openUserMcp', "Open User Configuration"),942category,943f1: true944});945}946947protected override getURI(accessor: ServicesAccessor): Promise<URI> {948const userDataProfileService = accessor.get(IUserDataProfileService);949return Promise.resolve(userDataProfileService.currentProfile.mcpResource);950}951}952953export class OpenRemoteUserMcpResourceCommand extends OpenMcpResourceCommand {954constructor() {955super({956id: McpCommandIds.OpenRemoteUserMcp,957title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User Configuration"),958category,959f1: true,960precondition: RemoteNameContext.notEqualsTo('')961});962}963964protected override async getURI(accessor: ServicesAccessor): Promise<URI> {965const userDataProfileService = accessor.get(IUserDataProfileService);966const remoteUserDataProfileService = accessor.get(IRemoteUserDataProfilesService);967const remoteProfile = await remoteUserDataProfileService.getRemoteProfile(userDataProfileService.currentProfile);968return remoteProfile.mcpResource;969}970}971972export class OpenWorkspaceFolderMcpResourceCommand extends Action2 {973constructor() {974super({975id: McpCommandIds.OpenWorkspaceFolderMcp,976title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Configuration"),977category,978f1: true,979precondition: WorkspaceFolderCountContext.notEqualsTo(0)980});981}982983async run(accessor: ServicesAccessor) {984const workspaceContextService = accessor.get(IWorkspaceContextService);985const commandService = accessor.get(ICommandService);986const editorService = accessor.get(IEditorService);987const workspaceFolders = workspaceContextService.getWorkspace().folders;988const workspaceFolder = workspaceFolders.length === 1 ? workspaceFolders[0] : await commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);989if (workspaceFolder) {990await editorService.openEditor({ resource: workspaceFolder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]) });991}992}993}994995export class OpenWorkspaceMcpResourceCommand extends Action2 {996constructor() {997super({998id: McpCommandIds.OpenWorkspaceMcp,999title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Configuration"),1000category,1001f1: true,1002precondition: WorkbenchStateContext.isEqualTo('workspace')1003});1004}10051006async run(accessor: ServicesAccessor) {1007const workspaceContextService = accessor.get(IWorkspaceContextService);1008const editorService = accessor.get(IEditorService);1009const workspaceConfiguration = workspaceContextService.getWorkspace().configuration;1010if (workspaceConfiguration) {1011await editorService.openEditor({ resource: workspaceConfiguration });1012}1013}1014}10151016export class McpBrowseResourcesCommand extends Action2 {1017constructor() {1018super({1019id: McpCommandIds.BrowseResources,1020title: localize2('mcp.browseResources', "Browse Resources..."),1021category,1022precondition: McpContextKeys.serverCount.greater(0),1023f1: true,1024});1025}10261027run(accessor: ServicesAccessor, server?: IMcpServer): void {1028if (server) {1029accessor.get(IInstantiationService).createInstance(McpResourceQuickPick, server).pick();1030} else {1031accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);1032}1033}1034}10351036export class McpConfigureSamplingModels extends Action2 {1037constructor() {1038super({1039id: McpCommandIds.ConfigureSamplingModels,1040title: localize2('mcp.configureSamplingModels', "Configure SamplingModel"),1041category,1042});1043}10441045async run(accessor: ServicesAccessor, server: IMcpServer): Promise<number> {1046const quickInputService = accessor.get(IQuickInputService);1047const lmService = accessor.get(ILanguageModelsService);1048const mcpSampling = accessor.get(IMcpSamplingService);10491050const existingIds = new Set(mcpSampling.getConfig(server).allowedModels);1051const allItems: IQuickPickItem[] = lmService.getLanguageModelIds().map(id => {1052const model = lmService.lookupLanguageModel(id)!;1053if (!model.isUserSelectable) {1054return undefined;1055}1056return {1057label: model.name,1058description: model.tooltip,1059id,1060picked: existingIds.size ? existingIds.has(id) : model.isDefault,1061};1062}).filter(isDefined);10631064allItems.sort((a, b) => (b.picked ? 1 : 0) - (a.picked ? 1 : 0) || a.label.localeCompare(b.label));10651066// do the quickpick selection1067const picked = await quickInputService.pick(allItems, {1068placeHolder: localize('mcp.configureSamplingModels.ph', 'Pick the models {0} can access via MCP sampling', server.definition.label),1069canPickMany: true,1070});10711072if (picked) {1073await mcpSampling.updateConfig(server, c => c.allowedModels = picked.map(p => p.id!));1074}10751076return picked?.length || 0;1077}1078}10791080export class McpStartPromptingServerCommand extends Action2 {1081constructor() {1082super({1083id: McpCommandIds.StartPromptForServer,1084title: localize2('mcp.startPromptingServer', "Start Prompting Server"),1085category,1086f1: false,1087});1088}10891090async run(accessor: ServicesAccessor, server: IMcpServer): Promise<void> {1091const widget = await openPanelChatAndGetWidget(accessor.get(IViewsService), accessor.get(IChatWidgetService));1092if (!widget) {1093return;1094}10951096const editor = widget.inputEditor;1097const model = editor.getModel();1098if (!model) {1099return;1100}11011102const range = (editor.getSelection() || model.getFullModelRange()).collapseToEnd();1103const text = mcpPromptPrefix(server.definition) + '.';11041105model.applyEdits([{ range, text }]);1106editor.setSelection(Range.fromPositions(range.getEndPosition().delta(0, text.length)));1107widget.focusInput();1108SuggestController.get(editor)?.triggerSuggest();1109}1110}111111121113