Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts
5334 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 } 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 { createMarkdownCommandLink, 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 { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';34import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';35import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';36import { StorageScope } from '../../../../platform/storage/common/storage.js';37import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';38import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js';39import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';40import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';41import { ActiveEditorContext, RemoteNameContext, ResourceContextKey, WorkbenchStateContext, WorkspaceFolderCountContext } from '../../../common/contextkeys.js';42import { IWorkbenchContribution } from '../../../common/contributions.js';43import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';44import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';45import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';46import { IEditorService } from '../../../services/editor/common/editorService.js';47import { IRemoteUserDataProfilesService } from '../../../services/userDataProfile/common/remoteUserDataProfiles.js';48import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';49import { IViewsService } from '../../../services/views/common/viewsService.js';50import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js';51import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js';52import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';53import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService/chatService.js';54import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js';55import { ILanguageModelsService } from '../../chat/common/languageModels.js';56import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js';57import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';58import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';59import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';60import { McpCommandIds } from '../common/mcpCommandIds.js';61import { McpContextKeys } from '../common/mcpContextKeys.js';62import { IMcpRegistry } from '../common/mcpRegistryTypes.js';63import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js';64import { McpAddConfigurationCommand, McpInstallFromManifestCommand } from './mcpCommandsAddConfiguration.js';65import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';66import { startServerAndWaitForLiveTools } from '../common/mcpTypesUtils.js';67import './media/mcpServerAction.css';68import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';6970// acroynms do not get localized71const category: ILocalizedString = {72original: 'MCP',73value: 'MCP',74};7576export class ListMcpServerCommand extends Action2 {77constructor() {78super({79id: McpCommandIds.ListServer,80title: localize2('mcp.list', 'List Servers'),81icon: Codicon.server,82category,83f1: true,84precondition: ChatContextKeys.Setup.hidden.negate(),85menu: [{86when: ContextKeyExpr.and(87ContextKeyExpr.or(88ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools),89McpContextKeys.hasServersWithErrors,90),91ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),92ChatContextKeys.lockedToCodingAgent.negate(),93ChatContextKeys.Setup.hidden.negate(),94),95id: MenuId.ChatInput,96group: 'navigation',97order: 101,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');112113mcpService.activateCollections();114115store.add(pick);116117store.add(autorun(reader => {118const servers = groupBy(mcpService.servers.read(reader).slice().sort((a, b) => (a.collection.presentation?.order || 0) - (b.collection.presentation?.order || 0)), s => s.collection.id);119const firstRun = pick.items.length === 0;120pick.items = [121{ 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) },122...Object.values(servers).filter(s => s!.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [123{ type: 'separator', label: servers![0].collection.label, id: servers![0].collection.id },124...servers!.map(server => ({125id: server.definition.id,126label: server.definition.label,127description: McpConnectionState.toString(server.connectionState.read(reader)),128})),129]),130];131132if (firstRun && pick.items.length > 3) {133pick.activeItems = pick.items.slice(2, 3) as ItemType[]; // select the first server by default134}135}));136137138const picked = await new Promise<ItemType | undefined>(resolve => {139store.add(pick.onDidAccept(() => {140resolve(pick.activeItems[0]);141}));142store.add(pick.onDidHide(() => {143resolve(undefined);144}));145pick.show();146});147148store.dispose();149150if (!picked) {151// no-op152} else if (picked.id === '$add') {153commandService.executeCommand(McpCommandIds.AddConfiguration);154} else {155commandService.executeCommand(McpCommandIds.ServerOptions, picked.id);156}157}158}159160interface ActionItem extends IQuickPickItem {161action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config' | 'configSampling' | 'samplingLog' | 'resources';162}163164interface AuthActionItem extends IQuickPickItem {165action: 'disconnect' | 'signout';166accountQuery: IAccountQuery;167}168169export class McpConfirmationServerOptionsCommand extends Action2 {170constructor() {171super({172id: McpCommandIds.ServerOptionsInConfirmation,173title: localize2('mcp.options', 'Server Options'),174category,175icon: Codicon.settingsGear,176f1: false,177menu: [{178id: MenuId.ChatConfirmationMenu,179when: ContextKeyExpr.and(180ContextKeyExpr.equals('chatConfirmationPartSource', 'mcp'),181ContextKeyExpr.or(182ContextKeyExpr.equals('chatConfirmationPartType', 'chatToolConfirmation'),183ContextKeyExpr.equals('chatConfirmationPartType', 'elicitation'),184),185),186group: 'navigation'187}],188});189}190191override async run(accessor: ServicesAccessor, arg: IChatToolInvocation | IChatElicitationRequest): Promise<void> {192const toolsService = accessor.get(ILanguageModelToolsService);193if (arg.kind === 'toolInvocation') {194const tool = toolsService.getTool(arg.toolId);195if (tool?.source.type === 'mcp') {196accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, tool.source.definitionId);197}198} else if (arg.kind === 'elicitation2') {199if (arg.source?.type === 'mcp') {200accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, arg.source.definitionId);201}202} else {203assertNever(arg);204}205}206}207208export class McpServerOptionsCommand extends Action2 {209constructor() {210super({211id: McpCommandIds.ServerOptions,212title: localize2('mcp.options', 'Server Options'),213category,214f1: false,215});216}217218override async run(accessor: ServicesAccessor, id: string): Promise<void> {219const mcpService = accessor.get(IMcpService);220const quickInputService = accessor.get(IQuickInputService);221const mcpRegistry = accessor.get(IMcpRegistry);222const editorService = accessor.get(IEditorService);223const commandService = accessor.get(ICommandService);224const samplingService = accessor.get(IMcpSamplingService);225const authenticationQueryService = accessor.get(IAuthenticationQueryService);226const authenticationService = accessor.get(IAuthenticationService);227const server = mcpService.servers.get().find(s => s.definition.id === id);228if (!server) {229return;230}231232const collection = mcpRegistry.collections.get().find(c => c.id === server.collection.id);233const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);234235const items: (ActionItem | AuthActionItem | IQuickPickSeparator)[] = [];236const serverState = server.connectionState.get();237238items.push({ type: 'separator', label: localize('mcp.actions.status', 'Status') });239240// Only show start when server is stopped or in error state241if (McpConnectionState.canBeStarted(serverState.state)) {242items.push({243label: localize('mcp.start', 'Start Server'),244action: 'start'245});246} else {247items.push({248label: localize('mcp.stop', 'Stop Server'),249action: 'stop'250});251items.push({252label: localize('mcp.restart', 'Restart Server'),253action: 'restart'254});255}256257items.push(...this._getAuthActions(authenticationQueryService, server.definition.id));258259const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;260if (configTarget) {261items.push({262label: localize('mcp.config', 'Show Configuration'),263action: 'config',264});265}266267items.push({268label: localize('mcp.showOutput', 'Show Output'),269action: 'showOutput'270});271272items.push(273{ type: 'separator', label: localize('mcp.actions.sampling', 'Sampling') },274{275label: localize('mcp.configAccess', 'Configure Model Access'),276description: localize('mcp.showOutput.description', 'Set the models the server can use via MCP sampling'),277action: 'configSampling'278},279);280281282if (samplingService.hasLogs(server)) {283items.push({284label: localize('mcp.samplingLog', 'Show Sampling Requests'),285description: localize('mcp.samplingLog.description', 'Show the sampling requests for this server'),286action: 'samplingLog',287});288}289290const capabilities = server.capabilities.get();291if (capabilities === undefined || (capabilities & McpCapability.Resources)) {292items.push({ type: 'separator', label: localize('mcp.actions.resources', 'Resources') });293items.push({294label: localize('mcp.resources', 'Browse Resources'),295action: 'resources',296});297}298299const pick = await quickInputService.pick(items, {300placeHolder: localize('mcp.selectAction', 'Select action for \'{0}\'', server.definition.label),301});302303if (!pick) {304return;305}306307switch (pick.action) {308case 'start':309await server.start({ promptType: 'all-untrusted' });310server.showOutput();311break;312case 'stop':313await server.stop();314break;315case 'restart':316await server.stop();317await server.start({ promptType: 'all-untrusted' });318break;319case 'disconnect':320await server.stop();321await this._handleAuth(authenticationService, pick.accountQuery, server.definition, false);322break;323case 'signout':324await server.stop();325await this._handleAuth(authenticationService, pick.accountQuery, server.definition, true);326break;327case 'showOutput':328server.showOutput();329break;330case 'config':331editorService.openEditor({332resource: URI.isUri(configTarget) ? configTarget : configTarget!.uri,333options: { selection: URI.isUri(configTarget) ? undefined : configTarget!.range }334});335break;336case 'configSampling':337return commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);338case 'resources':339return commandService.executeCommand(McpCommandIds.BrowseResources, server);340case 'samplingLog':341editorService.openEditor({342resource: undefined,343contents: samplingService.getLogText(server),344label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),345});346break;347default:348assertNever(pick);349}350}351352private _getAuthActions(353authenticationQueryService: IAuthenticationQueryService,354serverId: string355): AuthActionItem[] {356const result: AuthActionItem[] = [];357// Really, this should only ever have one entry.358for (const [providerId, accountName] of authenticationQueryService.mcpServer(serverId).getAllAccountPreferences()) {359360const accountQuery = authenticationQueryService.provider(providerId).account(accountName);361if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {362continue; // skip accounts that are not allowed363}364// If there are multiple allowed servers/extensions, other things are using this provider365// so we show a disconnect action, otherwise we show a sign out action.366if (accountQuery.entities().getEntityCount().total > 1) {367result.push({368action: 'disconnect',369label: localize('mcp.disconnect', 'Disconnect Account'),370description: `(${accountName})`,371accountQuery372});373} else {374result.push({375action: 'signout',376label: localize('mcp.signOut', 'Sign Out'),377description: `(${accountName})`,378accountQuery379});380}381}382return result;383}384385private async _handleAuth(386authenticationService: IAuthenticationService,387accountQuery: IAccountQuery,388definition: McpDefinitionReference,389signOut: boolean390) {391const { providerId, accountName } = accountQuery;392accountQuery.mcpServer(definition.id).setAccessAllowed(false, definition.label);393if (signOut) {394const accounts = await authenticationService.getAccounts(providerId);395const account = accounts.find(a => a.label === accountName);396if (account) {397const sessions = await authenticationService.getSessions(providerId, undefined, { account });398for (const session of sessions) {399await authenticationService.removeSession(providerId, session.id);400}401}402}403}404}405406export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution {407constructor(408@IActionViewItemService actionViewItemService: IActionViewItemService,409@IMcpService mcpService: IMcpService,410@IInstantiationService instaService: IInstantiationService,411@ICommandService commandService: ICommandService,412@IConfigurationService configurationService: IConfigurationService,413) {414super();415416const hoverIsOpen = observableValue(this, false);417const config = observableConfigValue(mcpAutoStartConfig, McpAutoStartValue.NewAndOutdated, configurationService);418419const enum DisplayedState {420None,421NewTools,422Error,423Refreshing,424}425426type DisplayedStateT = {427state: DisplayedState;428servers: (IMcpServer | McpCollectionDefinition)[];429};430431function isServer(s: IMcpServer | McpCollectionDefinition): s is IMcpServer {432return typeof (s as IMcpServer).start === 'function';433}434435const displayedStateCurrent = derived((reader): DisplayedStateT => {436const servers = mcpService.servers.read(reader);437const serversPerState: (IMcpServer | McpCollectionDefinition)[][] = [];438for (const server of servers) {439let thisState = DisplayedState.None;440switch (server.cacheState.read(reader)) {441case McpServerCacheState.Unknown:442case McpServerCacheState.Outdated:443thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools;444break;445case McpServerCacheState.RefreshingFromUnknown:446thisState = DisplayedState.Refreshing;447break;448default:449thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None;450break;451}452453serversPerState[thisState] ??= [];454serversPerState[thisState].push(server);455}456457const unknownServerStates = mcpService.lazyCollectionState.read(reader);458if (unknownServerStates.state === LazyCollectionState.LoadingUnknown) {459serversPerState[DisplayedState.Refreshing] ??= [];460serversPerState[DisplayedState.Refreshing].push(...unknownServerStates.collections);461} else if (unknownServerStates.state === LazyCollectionState.HasUnknown) {462serversPerState[DisplayedState.NewTools] ??= [];463serversPerState[DisplayedState.NewTools].push(...unknownServerStates.collections);464}465466let maxState = (serversPerState.length - 1) as DisplayedState;467if (maxState === DisplayedState.NewTools && config.read(reader) !== McpAutoStartValue.Never) {468maxState = DisplayedState.None;469}470471return { state: maxState, servers: serversPerState[maxState] || [] };472});473474// avoid hiding the hover if a state changes while it's open:475const displayedState = derivedObservableWithCache<DisplayedStateT>(this, (reader, last) => {476if (last && hoverIsOpen.read(reader)) {477return last;478} else {479return displayedStateCurrent.read(reader);480}481});482483const actionItemState = displayedState.map(s => s.state);484485this._store.add(actionViewItemService.register(MenuId.ChatInput, McpCommandIds.ListServer, (action, options) => {486if (!(action instanceof MenuItemAction)) {487return undefined;488}489490return instaService.createInstance(class extends MenuEntryActionViewItem {491492override render(container: HTMLElement): void {493494super.render(container);495container.classList.add('chat-mcp');496container.style.position = 'relative';497498const stateIndicator = container.appendChild($('.chat-mcp-state-indicator'));499stateIndicator.style.display = 'none';500501this._register(autorun(r => {502const displayed = displayedState.read(r);503const { state } = displayed;504this.updateTooltip();505506507stateIndicator.ariaLabel = this.getLabelForState(displayed);508stateIndicator.className = 'chat-mcp-state-indicator';509if (state === DisplayedState.NewTools) {510stateIndicator.style.display = 'block';511stateIndicator.classList.add('chat-mcp-state-new', ...ThemeIcon.asClassNameArray(Codicon.refresh));512} else if (state === DisplayedState.Error) {513stateIndicator.style.display = 'block';514stateIndicator.classList.add('chat-mcp-state-error', ...ThemeIcon.asClassNameArray(Codicon.warning));515} else if (state === DisplayedState.Refreshing) {516stateIndicator.style.display = 'block';517stateIndicator.classList.add('chat-mcp-state-refreshing', ...ThemeIcon.asClassNameArray(spinningLoading));518} else {519stateIndicator.style.display = 'none';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) => createMarkdownCommandLink({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,609{ ...defaultCheckboxStyles }610));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.fromObservableLight(actionItemState)));650}651}652653export class ResetMcpTrustCommand extends Action2 {654constructor() {655super({656id: McpCommandIds.ResetTrust,657title: localize2('mcp.resetTrust', "Reset Trust"),658category,659f1: true,660precondition: ContextKeyExpr.and(McpContextKeys.toolsCount.greater(0), ChatContextKeys.Setup.hidden.negate()),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: ContextKeyExpr.and(McpContextKeys.toolsCount.greater(0), ChatContextKeys.Setup.hidden.negate()),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,698precondition: ChatContextKeys.Setup.hidden.negate(),699menu: {700id: MenuId.EditorContent,701when: ContextKeyExpr.and(702ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/),703ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID),704ChatContextKeys.Setup.hidden.negate(),705)706}707});708}709710async run(accessor: ServicesAccessor, configUri?: string): Promise<void> {711const instantiationService = accessor.get(IInstantiationService);712const workspaceService = accessor.get(IWorkspaceContextService);713const target = configUri ? workspaceService.getWorkspaceFolder(URI.parse(configUri)) : undefined;714return instantiationService.createInstance(McpAddConfigurationCommand, target ?? undefined).run();715}716}717718export class InstallFromManifestAction extends Action2 {719constructor() {720super({721id: McpCommandIds.InstallFromManifest,722title: localize2('mcp.installFromManifest', "Install Server from Manifest..."),723metadata: {724description: localize2('mcp.installFromManifest.description', "Install an MCP server from a JSON manifest file"),725},726category,727f1: true,728precondition: ChatContextKeys.Setup.hidden.negate(),729});730}731732async run(accessor: ServicesAccessor): Promise<void> {733const instantiationService = accessor.get(IInstantiationService);734return instantiationService.createInstance(McpInstallFromManifestCommand).run();735}736}737738739export class RemoveStoredInput extends Action2 {740constructor() {741super({742id: McpCommandIds.RemoveStoredInput,743title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),744category,745f1: false,746});747}748749run(accessor: ServicesAccessor, scope: StorageScope, id?: string): void {750accessor.get(IMcpRegistry).clearSavedInputs(scope, id);751}752}753754export class EditStoredInput extends Action2 {755constructor() {756super({757id: McpCommandIds.EditStoredInput,758title: localize2('mcp.editStoredInput', "Edit Stored Input"),759category,760f1: false,761});762}763764run(accessor: ServicesAccessor, inputId: string, uri: URI | undefined, configSection: string, target: ConfigurationTarget): void {765const workspaceFolder = uri && accessor.get(IWorkspaceContextService).getWorkspaceFolder(uri);766accessor.get(IMcpRegistry).editSavedInput(inputId, workspaceFolder || undefined, configSection, target);767}768}769770export class ShowConfiguration extends Action2 {771constructor() {772super({773id: McpCommandIds.ShowConfiguration,774title: localize2('mcp.command.showConfiguration', "Show Configuration"),775category,776f1: false,777});778}779780run(accessor: ServicesAccessor, collectionId: string, serverId: string): void {781const collection = accessor.get(IMcpRegistry).collections.get().find(c => c.id === collectionId);782if (!collection) {783return;784}785786const server = collection?.serverDefinitions.get().find(s => s.id === serverId);787const editorService = accessor.get(IEditorService);788if (server?.presentation?.origin) {789editorService.openEditor({790resource: server.presentation.origin.uri,791options: { selection: server.presentation.origin.range }792});793} else if (collection.presentation?.origin) {794editorService.openEditor({795resource: collection.presentation.origin,796});797}798}799}800801export class ShowOutput extends Action2 {802constructor() {803super({804id: McpCommandIds.ShowOutput,805title: localize2('mcp.command.showOutput', "Show Output"),806category,807f1: false,808});809}810811run(accessor: ServicesAccessor, serverId: string): void {812accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId)?.showOutput();813}814}815816export class RestartServer extends Action2 {817constructor() {818super({819id: McpCommandIds.RestartServer,820title: localize2('mcp.command.restartServer', "Restart Server"),821category,822f1: false,823});824}825826async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {827const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);828s?.showOutput();829await s?.stop();830await s?.start({ promptType: 'all-untrusted', ...opts });831}832}833834export class StartServer extends Action2 {835constructor() {836super({837id: McpCommandIds.StartServer,838title: localize2('mcp.command.startServer', "Start Server"),839category,840f1: false,841});842}843844async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts & { waitForLiveTools?: boolean }) {845let servers = accessor.get(IMcpService).servers.get();846if (serverId !== '*') {847servers = servers.filter(s => s.definition.id === serverId);848}849850const startOpts: IMcpServerStartOpts = { promptType: 'all-untrusted', ...opts };851if (opts?.waitForLiveTools) {852await Promise.all(servers.map(s => startServerAndWaitForLiveTools(s, startOpts)));853} else {854await Promise.all(servers.map(s => s.start(startOpts)));855}856}857}858859export class StopServer extends Action2 {860constructor() {861super({862id: McpCommandIds.StopServer,863title: localize2('mcp.command.stopServer', "Stop Server"),864category,865f1: false,866});867}868869async run(accessor: ServicesAccessor, serverId: string) {870const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);871await s?.stop();872}873}874875export class McpBrowseCommand extends Action2 {876constructor() {877super({878id: McpCommandIds.Browse,879title: localize2('mcp.command.browse', "MCP Servers"),880tooltip: localize2('mcp.command.browse.tooltip', "Browse MCP Servers"),881category,882icon: Codicon.search,883precondition: ChatContextKeys.Setup.hidden.negate(),884menu: [{885id: extensionsFilterSubMenu,886group: '1_predefined',887order: 1,888when: ChatContextKeys.Setup.hidden.negate(),889}, {890id: MenuId.ViewTitle,891when: ContextKeyExpr.and(ContextKeyExpr.equals('view', InstalledMcpServersViewId), ChatContextKeys.Setup.hidden.negate()),892group: 'navigation',893}],894});895}896897async run(accessor: ServicesAccessor) {898accessor.get(IExtensionsWorkbenchService).openSearch('@mcp ');899}900}901902MenuRegistry.appendMenuItem(MenuId.CommandPalette, {903command: {904id: McpCommandIds.Browse,905title: localize2('mcp.command.browse.mcp', "Browse MCP Servers"),906category,907precondition: ChatContextKeys.Setup.hidden.negate(),908},909});910911export class ShowInstalledMcpServersCommand extends Action2 {912constructor() {913super({914id: McpCommandIds.ShowInstalled,915title: localize2('mcp.command.show.installed', "Show Installed Servers"),916category,917precondition: ContextKeyExpr.and(HasInstalledMcpServersContext, ChatContextKeys.Setup.hidden.negate()),918f1: true,919});920}921922async run(accessor: ServicesAccessor) {923const viewsService = accessor.get(IViewsService);924const view = await viewsService.openView(InstalledMcpServersViewId, true);925if (!view) {926await viewsService.openViewContainer(VIEW_CONTAINER.id);927await viewsService.openView(InstalledMcpServersViewId, true);928}929}930}931932MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, {933command: {934id: McpCommandIds.ShowInstalled,935title: localize2('mcp.servers', "MCP Servers")936},937when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),938order: 10,939group: '2_level'940});941942abstract class OpenMcpResourceCommand extends Action2 {943protected abstract getURI(accessor: ServicesAccessor): Promise<URI>;944945async run(accessor: ServicesAccessor) {946const fileService = accessor.get(IFileService);947const editorService = accessor.get(IEditorService);948const resource = await this.getURI(accessor);949if (!(await fileService.exists(resource))) {950await fileService.createFile(resource, VSBuffer.fromString(JSON.stringify({ servers: {} }, null, '\t')));951}952await editorService.openEditor({ resource });953}954}955956export class OpenUserMcpResourceCommand extends OpenMcpResourceCommand {957constructor() {958super({959id: McpCommandIds.OpenUserMcp,960title: localize2('mcp.command.openUserMcp', "Open User Configuration"),961category,962f1: true,963precondition: ChatContextKeys.Setup.hidden.negate(),964});965}966967protected override getURI(accessor: ServicesAccessor): Promise<URI> {968const userDataProfileService = accessor.get(IUserDataProfileService);969return Promise.resolve(userDataProfileService.currentProfile.mcpResource);970}971}972973export class OpenRemoteUserMcpResourceCommand extends OpenMcpResourceCommand {974constructor() {975super({976id: McpCommandIds.OpenRemoteUserMcp,977title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User Configuration"),978category,979f1: true,980precondition: ContextKeyExpr.and(981ChatContextKeys.Setup.hidden.negate(),982RemoteNameContext.notEqualsTo('')983)984});985}986987protected override async getURI(accessor: ServicesAccessor): Promise<URI> {988const userDataProfileService = accessor.get(IUserDataProfileService);989const remoteUserDataProfileService = accessor.get(IRemoteUserDataProfilesService);990const remoteProfile = await remoteUserDataProfileService.getRemoteProfile(userDataProfileService.currentProfile);991return remoteProfile.mcpResource;992}993}994995export class OpenWorkspaceFolderMcpResourceCommand extends Action2 {996constructor() {997super({998id: McpCommandIds.OpenWorkspaceFolderMcp,999title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Configuration"),1000category,1001f1: true,1002precondition: ContextKeyExpr.and(1003ChatContextKeys.Setup.hidden.negate(),1004WorkspaceFolderCountContext.notEqualsTo(0)1005)1006});1007}10081009async run(accessor: ServicesAccessor) {1010const workspaceContextService = accessor.get(IWorkspaceContextService);1011const commandService = accessor.get(ICommandService);1012const editorService = accessor.get(IEditorService);1013const workspaceFolders = workspaceContextService.getWorkspace().folders;1014const workspaceFolder = workspaceFolders.length === 1 ? workspaceFolders[0] : await commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);1015if (workspaceFolder) {1016await editorService.openEditor({ resource: workspaceFolder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]) });1017}1018}1019}10201021export class OpenWorkspaceMcpResourceCommand extends Action2 {1022constructor() {1023super({1024id: McpCommandIds.OpenWorkspaceMcp,1025title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Configuration"),1026category,1027f1: true,1028precondition: ContextKeyExpr.and(1029ChatContextKeys.Setup.hidden.negate(),1030WorkbenchStateContext.isEqualTo('workspace')1031)1032});1033}10341035async run(accessor: ServicesAccessor) {1036const workspaceContextService = accessor.get(IWorkspaceContextService);1037const editorService = accessor.get(IEditorService);1038const workspaceConfiguration = workspaceContextService.getWorkspace().configuration;1039if (workspaceConfiguration) {1040await editorService.openEditor({ resource: workspaceConfiguration });1041}1042}1043}10441045export class McpBrowseResourcesCommand extends Action2 {1046constructor() {1047super({1048id: McpCommandIds.BrowseResources,1049title: localize2('mcp.browseResources', "Browse Resources..."),1050category,1051precondition: ContextKeyExpr.and(McpContextKeys.serverCount.greater(0), ChatContextKeys.Setup.hidden.negate()),1052f1: true,1053});1054}10551056run(accessor: ServicesAccessor, server?: IMcpServer): void {1057if (server) {1058accessor.get(IInstantiationService).createInstance(McpResourceQuickPick, server).pick();1059} else {1060accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);1061}1062}1063}10641065export class McpConfigureSamplingModels extends Action2 {1066constructor() {1067super({1068id: McpCommandIds.ConfigureSamplingModels,1069title: localize2('mcp.configureSamplingModels', "Configure SamplingModel"),1070category,1071});1072}10731074async run(accessor: ServicesAccessor, server: IMcpServer): Promise<number> {1075const quickInputService = accessor.get(IQuickInputService);1076const lmService = accessor.get(ILanguageModelsService);1077const mcpSampling = accessor.get(IMcpSamplingService);10781079const existingIds = new Set(mcpSampling.getConfig(server).allowedModels);1080const allItems: IQuickPickItem[] = lmService.getLanguageModelIds().map(id => {1081const model = lmService.lookupLanguageModel(id)!;1082if (!model.isUserSelectable) {1083return undefined;1084}1085return {1086label: model.name,1087description: model.tooltip,1088id,1089picked: existingIds.size ? existingIds.has(id) : model.isDefaultForLocation[ChatAgentLocation.Chat],1090};1091}).filter(isDefined);10921093allItems.sort((a, b) => (b.picked ? 1 : 0) - (a.picked ? 1 : 0) || a.label.localeCompare(b.label));10941095// do the quickpick selection1096const picked = await quickInputService.pick(allItems, {1097placeHolder: localize('mcp.configureSamplingModels.ph', 'Pick the models {0} can access via MCP sampling', server.definition.label),1098canPickMany: true,1099});11001101if (picked) {1102await mcpSampling.updateConfig(server, c => c.allowedModels = picked.map(p => p.id!));1103}11041105return picked?.length || 0;1106}1107}11081109export class McpStartPromptingServerCommand extends Action2 {1110constructor() {1111super({1112id: McpCommandIds.StartPromptForServer,1113title: localize2('mcp.startPromptingServer', "Start Prompting Server"),1114category,1115f1: false,1116});1117}11181119async run(accessor: ServicesAccessor, server: IMcpServer): Promise<void> {1120const widget = await openPanelChatAndGetWidget(accessor.get(IViewsService), accessor.get(IChatWidgetService));1121if (!widget) {1122return;1123}11241125const editor = widget.inputEditor;1126const model = editor.getModel();1127if (!model) {1128return;1129}11301131const range = (editor.getSelection() || model.getFullModelRange()).collapseToEnd();1132const text = mcpPromptPrefix(server.definition) + '.';11331134model.applyEdits([{ range, text }]);1135editor.setSelection(Range.fromPositions(range.getEndPosition().delta(0, text.length)));1136widget.focusInput();1137SuggestController.get(editor)?.triggerSuggest();1138}1139}11401141export class McpSkipCurrentAutostartCommand extends Action2 {1142constructor() {1143super({1144id: McpCommandIds.SkipCurrentAutostart,1145title: localize2('mcp.skipCurrentAutostart', "Skip Current Autostart"),1146category,1147f1: false,1148});1149}11501151async run(accessor: ServicesAccessor): Promise<void> {1152accessor.get(IMcpService).cancelAutostart();1153}1154}115511561157