Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts
5281 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 { getDomNodePagePosition } from '../../../../base/browser/dom.js';6import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';7import { alert } from '../../../../base/browser/ui/aria/aria.js';8import { Action, IAction, IActionChangeEvent, Separator } from '../../../../base/common/actions.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { IMarkdownString } from '../../../../base/common/htmlContent.js';11import { disposeIfDisposable } from '../../../../base/common/lifecycle.js';12import { ThemeIcon } from '../../../../base/common/themables.js';13import { URI } from '../../../../base/common/uri.js';14import { localize } from '../../../../nls.js';15import { Location } from '../../../../editor/common/languages.js';16import { ICommandService } from '../../../../platform/commands/common/commands.js';17import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';18import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';19import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';20import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';21import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';22import { IEditorService } from '../../../services/editor/common/editorService.js';23import { errorIcon, infoIcon, manageExtensionIcon, trustIcon, warningIcon } from '../../extensions/browser/extensionsIcons.js';24import { McpCommandIds } from '../common/mcpCommandIds.js';25import { IMcpRegistry } from '../common/mcpRegistryTypes.js';26import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab, McpServerInstallState } from '../common/mcpTypes.js';27import { startServerByFilter } from '../common/mcpTypesUtils.js';28import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';29import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';30import { IQuickInputService, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';31import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';32import { Schemas } from '../../../../base/common/network.js';33import { ILabelService } from '../../../../platform/label/common/label.js';34import { LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';35import { ExtensionAction } from '../../extensions/browser/extensionsActions.js';36import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';37import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js';38import Severity from '../../../../base/common/severity.js';3940export interface IMcpServerActionChangeEvent extends IActionChangeEvent {41readonly hidden?: boolean;42readonly menuActions?: IAction[];43}4445export abstract class McpServerAction extends Action implements IMcpServerContainer {4647protected override _onDidChange = this._register(new Emitter<IMcpServerActionChangeEvent>());48override get onDidChange() { return this._onDidChange.event; }4950static readonly EXTENSION_ACTION_CLASS = 'extension-action';51static readonly TEXT_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} text`;52static readonly LABEL_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} label`;53static readonly PROMINENT_LABEL_ACTION_CLASS = `${McpServerAction.LABEL_ACTION_CLASS} prominent`;54static readonly ICON_ACTION_CLASS = `${McpServerAction.EXTENSION_ACTION_CLASS} icon`;5556private _hidden: boolean = false;57get hidden(): boolean { return this._hidden; }58set hidden(hidden: boolean) {59if (this._hidden !== hidden) {60this._hidden = hidden;61this._onDidChange.fire({ hidden });62}63}6465protected override _setEnabled(value: boolean): void {66super._setEnabled(value);67if (this.hideOnDisabled) {68this.hidden = !value;69}70}7172protected hideOnDisabled: boolean = true;7374private _mcpServer: IWorkbenchMcpServer | null = null;75get mcpServer(): IWorkbenchMcpServer | null { return this._mcpServer; }76set mcpServer(mcpServer: IWorkbenchMcpServer | null) { this._mcpServer = mcpServer; this.update(); }7778abstract update(): void;79}8081export class ButtonWithDropDownExtensionAction extends McpServerAction {8283private primaryAction: IAction | undefined;8485readonly menuActionClassNames: string[] = [];86private _menuActions: IAction[] = [];87get menuActions(): IAction[] { return [...this._menuActions]; }8889override get mcpServer(): IWorkbenchMcpServer | null {90return super.mcpServer;91}9293override set mcpServer(mcpServer: IWorkbenchMcpServer | null) {94this.actions.forEach(a => a.mcpServer = mcpServer);95super.mcpServer = mcpServer;96}9798protected readonly actions: McpServerAction[];99100constructor(101id: string,102clazz: string,103private readonly actionsGroups: McpServerAction[][],104) {105clazz = `${clazz} action-dropdown`;106super(id, undefined, clazz);107this.menuActionClassNames = clazz.split(' ');108this.hideOnDisabled = false;109this.actions = actionsGroups.flat();110this.update();111this._register(Event.any(...this.actions.map(a => a.onDidChange))(() => this.update(true)));112this.actions.forEach(a => this._register(a));113}114115update(donotUpdateActions?: boolean): void {116if (!donotUpdateActions) {117this.actions.forEach(a => a.update());118}119120const actionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => !a.hidden));121122let actions: IAction[] = [];123for (const visibleActions of actionsGroups) {124if (visibleActions.length) {125actions = [...actions, ...visibleActions, new Separator()];126}127}128actions = actions.length ? actions.slice(0, actions.length - 1) : actions;129130this.primaryAction = actions[0];131this._menuActions = actions.length > 1 ? actions : [];132this._onDidChange.fire({ menuActions: this._menuActions });133134if (this.primaryAction) {135this.enabled = this.primaryAction.enabled;136this.label = this.getLabel(this.primaryAction as ExtensionAction);137this.tooltip = this.primaryAction.tooltip;138} else {139this.enabled = false;140}141}142143override async run(): Promise<void> {144if (this.enabled) {145await this.primaryAction?.run();146}147}148149protected getLabel(action: ExtensionAction): string {150return action.label;151}152}153154export class ButtonWithDropdownExtensionActionViewItem extends ActionWithDropdownActionViewItem {155156constructor(157action: ButtonWithDropDownExtensionAction,158options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions,159contextMenuProvider: IContextMenuProvider160) {161super(null, action, options, contextMenuProvider);162this._register(action.onDidChange(e => {163if (e.hidden !== undefined || e.menuActions !== undefined) {164this.updateClass();165}166}));167}168169override render(container: HTMLElement): void {170super.render(container);171this.updateClass();172}173174protected override updateClass(): void {175super.updateClass();176if (this.element && this.dropdownMenuActionViewItem?.element) {177this.element.classList.toggle('hide', (<ButtonWithDropDownExtensionAction>this._action).hidden);178const isMenuEmpty = (<ButtonWithDropDownExtensionAction>this._action).menuActions.length === 0;179this.element.classList.toggle('empty', isMenuEmpty);180this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty);181}182}183184}185186export abstract class DropDownAction extends McpServerAction {187188constructor(189id: string,190label: string,191cssClass: string,192enabled: boolean,193@IInstantiationService protected instantiationService: IInstantiationService194) {195super(id, label, cssClass, enabled);196}197198private _actionViewItem: DropDownExtensionActionViewItem | null = null;199createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem {200this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options);201return this._actionViewItem;202}203204public override run(actionGroups: IAction[][]): Promise<void> {205this._actionViewItem?.showMenu(actionGroups);206return Promise.resolve();207}208}209210export class DropDownExtensionActionViewItem extends ActionViewItem {211212constructor(213action: IAction,214options: IActionViewItemOptions,215@IContextMenuService private readonly contextMenuService: IContextMenuService216) {217super(null, action, { ...options, icon: true, label: true });218}219220public showMenu(menuActionGroups: IAction[][]): void {221if (this.element) {222const actions = this.getActions(menuActionGroups);223const elementPosition = getDomNodePagePosition(this.element);224const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 };225this.contextMenuService.showContextMenu({226getAnchor: () => anchor,227getActions: () => actions,228actionRunner: this.actionRunner,229onHide: () => disposeIfDisposable(actions)230});231}232}233234private getActions(menuActionGroups: IAction[][]): IAction[] {235let actions: IAction[] = [];236for (const menuActions of menuActionGroups) {237actions = [...actions, ...menuActions, new Separator()];238}239return actions.length ? actions.slice(0, actions.length - 1) : actions;240}241}242243export class InstallAction extends McpServerAction {244245static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;246private static readonly HIDE = `${this.CLASS} hide`;247248constructor(249private readonly open: boolean,250@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,251@ITelemetryService private readonly telemetryService: ITelemetryService,252@IMcpService private readonly mcpService: IMcpService,253) {254super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false);255this.update();256}257258update(): void {259this.enabled = false;260this.class = InstallAction.HIDE;261if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {262return;263}264if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) {265return;266}267this.class = InstallAction.CLASS;268this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;269}270271override async run(): Promise<void> {272if (!this.mcpServer) {273return;274}275276if (this.open) {277this.mcpWorkbenchService.open(this.mcpServer);278alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));279}280281type McpServerInstallClassification = {282owner: 'sandy081';283comment: 'Used to understand if the action to install the MCP server is used.';284name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };285};286type McpServerInstall = {287name?: string;288};289this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install', { name: this.mcpServer.gallery?.name });290291const installed = await this.mcpWorkbenchService.install(this.mcpServer);292293await startServerByFilter(this.mcpService, s => {294return s.definition.label === installed.name;295});296}297}298299export class InstallInWorkspaceAction extends McpServerAction {300301static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;302private static readonly HIDE = `${this.CLASS} hide`;303304constructor(305private readonly open: boolean,306@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,307@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,308@IQuickInputService private readonly quickInputService: IQuickInputService,309@ITelemetryService private readonly telemetryService: ITelemetryService,310@IMcpService private readonly mcpService: IMcpService,311) {312super('extensions.installWorkspace', localize('installInWorkspace', "Install in Workspace"), InstallAction.CLASS, false);313this.update();314}315316update(): void {317this.enabled = false;318this.class = InstallInWorkspaceAction.HIDE;319if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) {320return;321}322if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {323return;324}325if (this.mcpServer.installState !== McpServerInstallState.Uninstalled && this.mcpServer.local?.scope === LocalMcpServerScope.Workspace) {326return;327}328this.class = InstallAction.CLASS;329this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;330}331332override async run(): Promise<void> {333if (!this.mcpServer) {334return;335}336337if (this.open) {338this.mcpWorkbenchService.open(this.mcpServer, { preserveFocus: true });339alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));340}341342const target = await this.getConfigurationTarget();343if (!target) {344return;345}346347type McpServerInstallClassification = {348owner: 'sandy081';349comment: 'Used to understand if the action to install the MCP server is used.';350name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };351};352type McpServerInstall = {353name?: string;354};355this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install:workspace', { name: this.mcpServer.gallery?.name });356357const installed = await this.mcpWorkbenchService.install(this.mcpServer, { target });358await startServerByFilter(this.mcpService, s => {359return s.definition.label === installed.name;360});361}362363private async getConfigurationTarget(): Promise<ConfigurationTarget | IWorkspaceFolder | undefined> {364type OptionQuickPickItem = QuickPickItem & { target?: ConfigurationTarget | IWorkspaceFolder };365const options: OptionQuickPickItem[] = [];366367for (const folder of this.workspaceService.getWorkspace().folders) {368options.push({ target: folder, label: folder.name, description: localize('install in workspace folder', "Workspace Folder") });369}370371if (this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) {372if (options.length > 0) {373options.push({ type: 'separator' });374}375options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace") });376}377378if (options.length === 1) {379return options[0].target;380}381382const targetPick = await this.quickInputService.pick(options, {383title: localize('mcp.target.title', "Choose where to install the MCP server"),384});385386return (targetPick as OptionQuickPickItem)?.target;387}388}389390export class InstallInRemoteAction extends McpServerAction {391392static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`;393private static readonly HIDE = `${this.CLASS} hide`;394395constructor(396private readonly open: boolean,397@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,398@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,399@ITelemetryService private readonly telemetryService: ITelemetryService,400@ILabelService private readonly labelService: ILabelService,401@IMcpService private readonly mcpService: IMcpService,402) {403super('extensions.installRemote', localize('installInRemote', "Install (Remote)"), InstallAction.CLASS, false);404const remoteLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority);405this.label = localize('installInRemoteLabel', "Install in {0}", remoteLabel);406this.update();407}408409update(): void {410this.enabled = false;411this.class = InstallInRemoteAction.HIDE;412if (!this.environmentService.remoteAuthority) {413return;414}415if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {416return;417}418if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) {419if (this.mcpServer.local?.scope === LocalMcpServerScope.RemoteUser) {420return;421}422if (this.mcpWorkbenchService.local.find(mcpServer => mcpServer.name === this.mcpServer?.name && mcpServer.local?.scope === LocalMcpServerScope.RemoteUser)) {423return;424}425}426this.class = InstallAction.CLASS;427this.enabled = this.mcpWorkbenchService.canInstall(this.mcpServer) === true;428}429430override async run(): Promise<void> {431if (!this.mcpServer) {432return;433}434435if (this.open) {436this.mcpWorkbenchService.open(this.mcpServer);437alert(localize('mcpServerInstallation', "Installing MCP Server {0} started. An editor is now open with more details on this MCP Server", this.mcpServer.label));438}439440type McpServerInstallClassification = {441owner: 'sandy081';442comment: 'Used to understand if the action to install the MCP server is used.';443name?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The gallery name of the MCP server being installed' };444};445type McpServerInstall = {446name?: string;447};448this.telemetryService.publicLog2<McpServerInstall, McpServerInstallClassification>('mcp:action:install:remote', { name: this.mcpServer.gallery?.name });449450const installed = await this.mcpWorkbenchService.install(this.mcpServer, { target: ConfigurationTarget.USER_REMOTE });451await startServerByFilter(this.mcpService, s => {452return s.definition.label === installed.name;453});454}455456}457458export class InstallingLabelAction extends McpServerAction {459460private static readonly LABEL = localize('installing', "Installing");461private static readonly CLASS = `${McpServerAction.LABEL_ACTION_CLASS} install installing`;462463constructor() {464super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false);465}466467update(): void {468this.class = `${InstallingLabelAction.CLASS}${this.mcpServer && this.mcpServer.installState === McpServerInstallState.Installing ? '' : ' hide'}`;469}470}471472export class UninstallAction extends McpServerAction {473474static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent uninstall`;475private static readonly HIDE = `${this.CLASS} hide`;476477constructor(478@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,479) {480super('extensions.uninstall', localize('uninstall', "Uninstall"), UninstallAction.CLASS, false);481this.update();482}483484update(): void {485this.enabled = false;486this.class = UninstallAction.HIDE;487if (!this.mcpServer) {488return;489}490if (!this.mcpServer.local) {491return;492}493if (this.mcpServer.installState !== McpServerInstallState.Installed) {494this.enabled = false;495return;496}497this.class = UninstallAction.CLASS;498this.enabled = true;499this.label = localize('uninstall', "Uninstall");500}501502override async run(): Promise<void> {503if (!this.mcpServer) {504return;505}506await this.mcpWorkbenchService.uninstall(this.mcpServer);507}508}509510export function getContextMenuActions(mcpServer: IWorkbenchMcpServer, isEditorAction: boolean, instantiationService: IInstantiationService): IAction[][] {511return instantiationService.invokeFunction(accessor => {512const workspaceService = accessor.get(IWorkspaceContextService);513const environmentService = accessor.get(IWorkbenchEnvironmentService);514515const groups: McpServerAction[][] = [];516const isInstalled = mcpServer.installState === McpServerInstallState.Installed;517518if (isInstalled) {519groups.push([520instantiationService.createInstance(StartServerAction),521]);522groups.push([523instantiationService.createInstance(StopServerAction),524instantiationService.createInstance(RestartServerAction),525]);526groups.push([527instantiationService.createInstance(AuthServerAction),528]);529groups.push([530instantiationService.createInstance(ShowServerOutputAction),531instantiationService.createInstance(ShowServerConfigurationAction),532instantiationService.createInstance(ShowServerJsonConfigurationAction),533]);534groups.push([535instantiationService.createInstance(ConfigureModelAccessAction),536instantiationService.createInstance(ShowSamplingRequestsAction),537]);538groups.push([539instantiationService.createInstance(BrowseResourcesAction),540]);541if (!isEditorAction) {542const installGroup: McpServerAction[] = [instantiationService.createInstance(UninstallAction)];543if (workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY) {544installGroup.push(instantiationService.createInstance(InstallInWorkspaceAction, false));545}546if (environmentService.remoteAuthority && mcpServer.local?.scope !== LocalMcpServerScope.RemoteUser) {547installGroup.push(instantiationService.createInstance(InstallInRemoteAction, false));548}549groups.push(installGroup);550}551} else {552const installGroup = [];553if (workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY) {554installGroup.push(instantiationService.createInstance(InstallInWorkspaceAction, !isEditorAction));555}556if (environmentService.remoteAuthority) {557installGroup.push(instantiationService.createInstance(InstallInRemoteAction, !isEditorAction));558}559groups.push(installGroup);560}561groups.forEach(group => group.forEach(extensionAction => extensionAction.mcpServer = mcpServer));562563return groups;564});565}566567export class ManageMcpServerAction extends DropDownAction {568569static readonly ID = 'mcpServer.manage';570571private static readonly Class = `${McpServerAction.ICON_ACTION_CLASS} manage ` + ThemeIcon.asClassName(manageExtensionIcon);572private static readonly HideManageExtensionClass = `${this.Class} hide`;573574constructor(575private readonly isEditorAction: boolean,576@IInstantiationService instantiationService: IInstantiationService,577) {578579super(ManageMcpServerAction.ID, '', '', true, instantiationService);580this.tooltip = localize('manage', "Manage");581this.update();582}583584override async run(): Promise<void> {585return super.run(this.mcpServer ? getContextMenuActions(this.mcpServer, this.isEditorAction, this.instantiationService) : []);586}587588update(): void {589this.class = ManageMcpServerAction.HideManageExtensionClass;590this.enabled = false;591if (!this.mcpServer) {592return;593}594if (this.isEditorAction) {595this.enabled = true;596this.class = ManageMcpServerAction.Class;597} else {598this.enabled = !!this.mcpServer.local;599this.class = this.enabled ? ManageMcpServerAction.Class : ManageMcpServerAction.HideManageExtensionClass;600}601}602}603604export class StartServerAction extends McpServerAction {605606static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent start`;607private static readonly HIDE = `${this.CLASS} hide`;608609constructor(610@IMcpService private readonly mcpService: IMcpService,611) {612super('extensions.start', localize('start', "Start Server"), StartServerAction.CLASS, false);613this.update();614}615616update(): void {617this.enabled = false;618this.class = StartServerAction.HIDE;619const server = this.getServer();620if (!server) {621return;622}623const serverState = server.connectionState.get();624if (!McpConnectionState.canBeStarted(serverState.state)) {625return;626}627this.class = StartServerAction.CLASS;628this.enabled = true;629this.label = localize('start', "Start Server");630}631632override async run(): Promise<void> {633const server = this.getServer();634if (!server) {635return;636}637await server.start({ promptType: 'all-untrusted' });638server.showOutput();639}640641private getServer(): IMcpServer | undefined {642if (!this.mcpServer) {643return;644}645if (!this.mcpServer.local) {646return;647}648return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);649}650}651652export class StopServerAction extends McpServerAction {653654static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent stop`;655private static readonly HIDE = `${this.CLASS} hide`;656657constructor(658@IMcpService private readonly mcpService: IMcpService,659) {660super('extensions.stop', localize('stop', "Stop Server"), StopServerAction.CLASS, false);661this.update();662}663664update(): void {665this.enabled = false;666this.class = StopServerAction.HIDE;667const server = this.getServer();668if (!server) {669return;670}671const serverState = server.connectionState.get();672if (McpConnectionState.canBeStarted(serverState.state)) {673return;674}675this.class = StopServerAction.CLASS;676this.enabled = true;677this.label = localize('stop', "Stop Server");678}679680override async run(): Promise<void> {681const server = this.getServer();682if (!server) {683return;684}685await server.stop();686}687688private getServer(): IMcpServer | undefined {689if (!this.mcpServer) {690return;691}692if (!this.mcpServer.local) {693return;694}695return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);696}697}698699export class RestartServerAction extends McpServerAction {700701static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent restart`;702private static readonly HIDE = `${this.CLASS} hide`;703704constructor(705@IMcpService private readonly mcpService: IMcpService,706) {707super('extensions.restart', localize('restart', "Restart Server"), RestartServerAction.CLASS, false);708this.update();709}710711update(): void {712this.enabled = false;713this.class = RestartServerAction.HIDE;714const server = this.getServer();715if (!server) {716return;717}718const serverState = server.connectionState.get();719if (McpConnectionState.canBeStarted(serverState.state)) {720return;721}722this.class = RestartServerAction.CLASS;723this.enabled = true;724this.label = localize('restart', "Restart Server");725}726727override async run(): Promise<void> {728const server = this.getServer();729if (!server) {730return;731}732await server.stop();733await server.start({ promptType: 'all-untrusted' });734server.showOutput();735}736737private getServer(): IMcpServer | undefined {738if (!this.mcpServer) {739return;740}741if (!this.mcpServer.local) {742return;743}744return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);745}746}747748export class AuthServerAction extends McpServerAction {749750static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent account`;751private static readonly HIDE = `${this.CLASS} hide`;752753private static readonly SIGN_OUT = localize('mcp.signOut', 'Sign Out');754private static readonly DISCONNECT = localize('mcp.disconnect', 'Disconnect Account');755756private _accountQuery: IAccountQuery | undefined;757758constructor(759@IMcpService private readonly mcpService: IMcpService,760@IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService,761@IAuthenticationService private readonly _authenticationService: IAuthenticationService762) {763super('extensions.restart', localize('restart', "Restart Server"), RestartServerAction.CLASS, false);764this.update();765}766767update(): void {768this.enabled = false;769this.class = AuthServerAction.HIDE;770const server = this.getServer();771if (!server) {772return;773}774const accountQuery = this.getAccountQuery();775if (!accountQuery) {776return;777}778this._accountQuery = accountQuery;779this.class = AuthServerAction.CLASS;780this.enabled = true;781let label = accountQuery.entities().getEntityCount().total > 1 ? AuthServerAction.DISCONNECT : AuthServerAction.SIGN_OUT;782label += ` (${accountQuery.accountName})`;783this.label = label;784}785786override async run(): Promise<void> {787const server = this.getServer();788if (!server) {789return;790}791const accountQuery = this.getAccountQuery();792if (!accountQuery) {793return;794}795await server.stop();796const { providerId, accountName } = accountQuery;797accountQuery.mcpServer(server.definition.id).setAccessAllowed(false, server.definition.label);798if (this.label === AuthServerAction.SIGN_OUT) {799const accounts = await this._authenticationService.getAccounts(providerId);800const account = accounts.find(a => a.label === accountName);801if (account) {802const sessions = await this._authenticationService.getSessions(providerId, undefined, { account });803for (const session of sessions) {804await this._authenticationService.removeSession(providerId, session.id);805}806}807}808}809810private getServer(): IMcpServer | undefined {811if (!this.mcpServer) {812return;813}814if (!this.mcpServer.local) {815return;816}817return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);818}819820private getAccountQuery(): IAccountQuery | undefined {821const server = this.getServer();822if (!server) {823return undefined;824}825if (this._accountQuery) {826return this._accountQuery;827}828const serverId = server.definition.id;829const preferences = this._authenticationQueryService.mcpServer(serverId).getAllAccountPreferences();830if (!preferences.size) {831return undefined;832}833for (const [providerId, accountName] of preferences) {834const accountQuery = this._authenticationQueryService.provider(providerId).account(accountName);835if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {836continue; // skip accounts that are not allowed837}838return accountQuery;839}840return undefined;841}842843}844845export class ShowServerOutputAction extends McpServerAction {846847static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent output`;848private static readonly HIDE = `${this.CLASS} hide`;849850constructor(851@IMcpService private readonly mcpService: IMcpService,852) {853super('extensions.output', localize('output', "Show Output"), ShowServerOutputAction.CLASS, false);854this.update();855}856857update(): void {858this.enabled = false;859this.class = ShowServerOutputAction.HIDE;860const server = this.getServer();861if (!server) {862return;863}864this.class = ShowServerOutputAction.CLASS;865this.enabled = true;866this.label = localize('output', "Show Output");867}868869override async run(): Promise<void> {870const server = this.getServer();871if (!server) {872return;873}874server.showOutput();875}876877private getServer(): IMcpServer | undefined {878if (!this.mcpServer) {879return;880}881if (!this.mcpServer.local) {882return;883}884return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);885}886}887888export class ShowServerConfigurationAction extends McpServerAction {889890static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;891private static readonly HIDE = `${this.CLASS} hide`;892893constructor(894@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService895) {896super('extensions.config', localize('config', "Show Configuration"), ShowServerConfigurationAction.CLASS, false);897this.update();898}899900update(): void {901this.enabled = false;902this.class = ShowServerConfigurationAction.HIDE;903if (!this.mcpServer?.local) {904return;905}906this.class = ShowServerConfigurationAction.CLASS;907this.enabled = true;908}909910override async run(): Promise<void> {911if (!this.mcpServer?.local) {912return;913}914this.mcpWorkbenchService.open(this.mcpServer, { tab: McpServerEditorTab.Configuration });915}916917}918919export class ShowServerJsonConfigurationAction extends McpServerAction {920921static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;922private static readonly HIDE = `${this.CLASS} hide`;923924constructor(925@IMcpService private readonly mcpService: IMcpService,926@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,927@IEditorService private readonly editorService: IEditorService,928) {929super('extensions.jsonConfig', localize('configJson', "Show Configuration (JSON)"), ShowServerJsonConfigurationAction.CLASS, false);930this.update();931}932933update(): void {934this.enabled = false;935this.class = ShowServerJsonConfigurationAction.HIDE;936const configurationTarget = this.getConfigurationTarget();937if (!configurationTarget) {938return;939}940this.class = ShowServerConfigurationAction.CLASS;941this.enabled = true;942}943944override async run(): Promise<void> {945const configurationTarget = this.getConfigurationTarget();946if (!configurationTarget) {947return;948}949this.editorService.openEditor({950resource: URI.isUri(configurationTarget) ? configurationTarget : configurationTarget!.uri,951options: { selection: URI.isUri(configurationTarget) ? undefined : configurationTarget!.range }952});953}954955private getConfigurationTarget(): Location | URI | undefined {956if (!this.mcpServer) {957return;958}959if (!this.mcpServer.local) {960return;961}962const server = this.mcpService.servers.get().find(s => s.definition.label === this.mcpServer?.name);963if (!server) {964return;965}966const collection = this.mcpRegistry.collections.get().find(c => c.id === server.collection.id);967const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);968return serverDefinition?.presentation?.origin || collection?.presentation?.origin;969}970}971972export class ConfigureModelAccessAction extends McpServerAction {973974static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;975private static readonly HIDE = `${this.CLASS} hide`;976977constructor(978@IMcpService private readonly mcpService: IMcpService,979@ICommandService private readonly commandService: ICommandService,980) {981super('extensions.config', localize('mcp.configAccess', 'Configure Model Access'), ConfigureModelAccessAction.CLASS, false);982this.update();983}984985update(): void {986this.enabled = false;987this.class = ConfigureModelAccessAction.HIDE;988const server = this.getServer();989if (!server) {990return;991}992this.class = ConfigureModelAccessAction.CLASS;993this.enabled = true;994this.label = localize('mcp.configAccess', 'Configure Model Access');995}996997override async run(): Promise<void> {998const server = this.getServer();999if (!server) {1000return;1001}1002this.commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);1003}10041005private getServer(): IMcpServer | undefined {1006if (!this.mcpServer) {1007return;1008}1009if (!this.mcpServer.local) {1010return;1011}1012return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);1013}1014}10151016export class ShowSamplingRequestsAction extends McpServerAction {10171018static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;1019private static readonly HIDE = `${this.CLASS} hide`;10201021constructor(1022@IMcpService private readonly mcpService: IMcpService,1023@IMcpSamplingService private readonly samplingService: IMcpSamplingService,1024@IEditorService private readonly editorService: IEditorService,1025) {1026super('extensions.config', localize('mcp.samplingLog', 'Show Sampling Requests'), ShowSamplingRequestsAction.CLASS, false);1027this.update();1028}10291030update(): void {1031this.enabled = false;1032this.class = ShowSamplingRequestsAction.HIDE;1033const server = this.getServer();1034if (!server) {1035return;1036}1037if (!this.samplingService.hasLogs(server)) {1038return;1039}1040this.class = ShowSamplingRequestsAction.CLASS;1041this.enabled = true;1042}10431044override async run(): Promise<void> {1045const server = this.getServer();1046if (!server) {1047return;1048}1049if (!this.samplingService.hasLogs(server)) {1050return;1051}1052this.editorService.openEditor({1053resource: undefined,1054contents: this.samplingService.getLogText(server),1055label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),1056});1057}10581059private getServer(): IMcpServer | undefined {1060if (!this.mcpServer) {1061return;1062}1063if (!this.mcpServer.local) {1064return;1065}1066return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);1067}1068}10691070export class BrowseResourcesAction extends McpServerAction {10711072static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent config`;1073private static readonly HIDE = `${this.CLASS} hide`;10741075constructor(1076@IMcpService private readonly mcpService: IMcpService,1077@ICommandService private readonly commandService: ICommandService,1078) {1079super('extensions.config', localize('mcp.resources', 'Browse Resources'), BrowseResourcesAction.CLASS, false);1080this.update();1081}10821083update(): void {1084this.enabled = false;1085this.class = BrowseResourcesAction.HIDE;1086const server = this.getServer();1087if (!server) {1088return;1089}1090const capabilities = server.capabilities.get();1091if (capabilities !== undefined && !(capabilities & McpCapability.Resources)) {1092return;1093}1094this.class = BrowseResourcesAction.CLASS;1095this.enabled = true;1096}10971098override async run(): Promise<void> {1099const server = this.getServer();1100if (!server) {1101return;1102}1103const capabilities = server.capabilities.get();1104if (capabilities !== undefined && !(capabilities & McpCapability.Resources)) {1105return;1106}1107return this.commandService.executeCommand(McpCommandIds.BrowseResources, server);1108}11091110private getServer(): IMcpServer | undefined {1111if (!this.mcpServer) {1112return;1113}1114if (!this.mcpServer.local) {1115return;1116}1117return this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id);1118}1119}11201121export type McpServerStatus = { readonly message: IMarkdownString; readonly icon?: ThemeIcon };11221123export class McpServerStatusAction extends McpServerAction {11241125private static readonly CLASS = `${McpServerAction.ICON_ACTION_CLASS} extension-status`;11261127private _status: McpServerStatus[] = [];1128get status(): McpServerStatus[] { return this._status; }11291130private readonly _onDidChangeStatus = this._register(new Emitter<void>());1131readonly onDidChangeStatus = this._onDidChangeStatus.event;11321133constructor(1134@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,1135@ICommandService private readonly commandService: ICommandService,1136) {1137super('extensions.status', '', `${McpServerStatusAction.CLASS} hide`, false);1138this.update();1139}11401141update(): void {1142this.computeAndUpdateStatus();1143}11441145private computeAndUpdateStatus(): void {1146this.updateStatus(undefined, true);1147this.enabled = false;11481149if (!this.mcpServer) {1150return;1151}11521153if ((this.mcpServer.gallery || this.mcpServer.installable) && this.mcpServer.installState === McpServerInstallState.Uninstalled) {1154const result = this.mcpWorkbenchService.canInstall(this.mcpServer);1155if (result !== true) {1156this.updateStatus({ icon: warningIcon, message: result }, true);1157return;1158}1159}11601161const runtimeState = this.mcpServer.runtimeStatus;1162if (runtimeState?.message) {1163this.updateStatus({ icon: runtimeState.message.severity === Severity.Warning ? warningIcon : runtimeState.message.severity === Severity.Error ? errorIcon : infoIcon, message: runtimeState.message.text }, true);1164}1165}11661167private updateStatus(status: McpServerStatus | undefined, updateClass: boolean): void {1168if (status) {1169if (this._status.some(s => s.message.value === status.message.value && s.icon?.id === status.icon?.id)) {1170return;1171}1172} else {1173if (this._status.length === 0) {1174return;1175}1176this._status = [];1177}11781179if (status) {1180this._status.push(status);1181this._status.sort((a, b) =>1182b.icon === trustIcon ? -1 :1183a.icon === trustIcon ? 1 :1184b.icon === errorIcon ? -1 :1185a.icon === errorIcon ? 1 :1186b.icon === warningIcon ? -1 :1187a.icon === warningIcon ? 1 :1188b.icon === infoIcon ? -1 :1189a.icon === infoIcon ? 1 :119001191);1192}11931194if (updateClass) {1195if (status?.icon === errorIcon) {1196this.class = `${McpServerStatusAction.CLASS} extension-status-error ${ThemeIcon.asClassName(errorIcon)}`;1197}1198else if (status?.icon === warningIcon) {1199this.class = `${McpServerStatusAction.CLASS} extension-status-warning ${ThemeIcon.asClassName(warningIcon)}`;1200}1201else if (status?.icon === infoIcon) {1202this.class = `${McpServerStatusAction.CLASS} extension-status-info ${ThemeIcon.asClassName(infoIcon)}`;1203}1204else if (status?.icon === trustIcon) {1205this.class = `${McpServerStatusAction.CLASS} ${ThemeIcon.asClassName(trustIcon)}`;1206}1207else {1208this.class = `${McpServerStatusAction.CLASS} hide`;1209}1210}1211this._onDidChangeStatus.fire();1212}12131214override async run(): Promise<void> {1215if (this._status[0]?.icon === trustIcon) {1216return this.commandService.executeCommand('workbench.trust.manage');1217}1218}1219}122012211222