mcp: add more data in codelens, clean up options pick (#250341)

Closes https://github.com/microsoft/vscode/issues/250126
This commit is contained in:
Connor Peet
2025-06-02 16:05:20 -07:00
committed by GitHub
parent 5a3960c91f
commit 02a362f436
7 changed files with 109 additions and 42 deletions
@@ -213,7 +213,7 @@ class SlashCommandCompletions extends Disposable {
return null;
}
const range = computeCompletionRanges(model, position, /\/\w*/g);
const range = computeCompletionRanges(model, position, /\/[\w.]*/g);
if (!range) {
return null;
}
@@ -35,7 +35,7 @@ import { McpSamplingService } from '../common/mcpSamplingService.js';
import { McpService } from '../common/mcpService.js';
import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { McpAddContextContribution } from './mcpAddContextContribution.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, MCPServerActionRendering, McpServerOptionsCommand, McpStartPromptingServerCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { McpDiscovery } from './mcpDiscovery.js';
import { McpLanguageFeatures } from './mcpLanguageFeatures.js';
import { McpResourceQuickAccess } from './mcpResourceQuickAccess.js';
@@ -79,6 +79,7 @@ registerAction2(ShowConfiguration);
registerAction2(McpBrowseCommand);
registerAction2(McpBrowseResourcesCommand);
registerAction2(McpConfigureSamplingModels);
registerAction2(McpStartPromptingServerCommand);
registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2('mcpAddContext', McpAddContextContribution, WorkbenchPhase.Eventually);
@@ -13,6 +13,8 @@ import { autorun, derived } from '../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { isDefined } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { Range } from '../../../../editor/common/core/range.js';
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
import { ILocalizedString, localize, localize2 } from '../../../../nls.js';
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
@@ -31,6 +33,7 @@ import { ActiveEditorContext, ResourceContextKey } from '../../../common/context
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js';
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
import { ChatMode } from '../../chat/common/constants.js';
import { ILanguageModelsService } from '../../chat/common/languageModels.js';
@@ -39,7 +42,7 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
import { McpContextKeys } from '../common/mcpContextKeys.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpConnectionState, McpServerCacheState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpConnectionState, mcpPromptPrefix, McpServerCacheState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
@@ -155,7 +158,6 @@ export class McpServerOptionsCommand extends Action2 {
const mcpRegistry = accessor.get(IMcpRegistry);
const editorService = accessor.get(IEditorService);
const commandService = accessor.get(ICommandService);
const instantiationService = accessor.get(IInstantiationService);
const samplingService = accessor.get(IMcpSamplingService);
const server = mcpService.servers.get().find(s => s.definition.id === id);
if (!server) {
@@ -169,9 +171,11 @@ export class McpServerOptionsCommand extends Action2 {
action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config' | 'configSampling' | 'samplingLog' | 'resources';
}
const items: ActionItem[] = [];
const items: (ActionItem | IQuickPickSeparator)[] = [];
const serverState = server.connectionState.get();
items.push({ type: 'separator', label: localize('mcp.actions.status', 'Status') });
// Only show start when server is stopped or in error state
if (McpConnectionState.canBeStarted(serverState.state)) {
items.push({
@@ -189,15 +193,21 @@ export class McpServerOptionsCommand extends Action2 {
});
}
const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;
if (configTarget) {
items.push({
label: localize('mcp.config', 'Show Configuration'),
action: 'config',
});
}
items.push({
label: localize('mcp.showOutput', 'Show Output'),
action: 'showOutput'
}, {
label: localize('mcp.configAccess', 'Configure Model Access'),
description: localize('mcp.showOutput.description', 'Set the models the server can use via MCP sampling'),
action: 'configSampling'
});
items.push({ type: 'separator', label: localize('mcp.actions.sampling', 'Sampling') });
if (samplingService.hasLogs(server)) {
items.push({
label: localize('mcp.samplingLog', 'Show Sampling Requests'),
@@ -208,20 +218,13 @@ export class McpServerOptionsCommand extends Action2 {
const capabilities = server.capabilities.get();
if (capabilities === undefined || (capabilities & McpCapability.Resources)) {
items.push({ type: 'separator', label: localize('mcp.actions.resources', 'Resources') });
items.push({
label: localize('mcp.resources', 'Browse Resources'),
action: 'resources',
});
}
const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;
if (configTarget) {
items.push({
label: localize('mcp.config', 'Show Configuration'),
action: 'config',
});
}
const pick = await quickInputService.pick(items, {
title: server.definition.label,
placeHolder: localize('mcp.selectAction', 'Select Server Action')
@@ -255,7 +258,7 @@ export class McpServerOptionsCommand extends Action2 {
case 'configSampling':
return commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);
case 'resources':
return instantiationService.createInstance(McpResourceQuickPick, server).pick();
return commandService.executeCommand(McpCommandIds.BrowseResources, server);
case 'samplingLog':
editorService.openEditor({
resource: undefined,
@@ -651,12 +654,15 @@ export class McpBrowseResourcesCommand extends Action2 {
});
}
run(accessor: ServicesAccessor): void {
accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);
run(accessor: ServicesAccessor, server?: IMcpServer): void {
if (server) {
accessor.get(IInstantiationService).createInstance(McpResourceQuickPick, server).pick();
} else {
accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);
}
}
}
export class McpConfigureSamplingModels extends Action2 {
constructor() {
super({
@@ -692,3 +698,38 @@ export class McpConfigureSamplingModels extends Action2 {
return picked?.length || 0;
}
}
export class McpStartPromptingServerCommand extends Action2 {
constructor() {
super({
id: McpCommandIds.StartPromptForServer,
title: localize2('mcp.startPromptingServer', "Start Prompting Server"),
category,
f1: false,
});
}
async run(accessor: ServicesAccessor, server: IMcpServer): Promise<void> {
const chatWidget = accessor.get(IChatWidgetService);
await accessor.get(IViewsService).openView(ChatViewId, true);
const widget = chatWidget.lastFocusedWidget || chatWidget.getAllWidgets()[0];
if (!widget) {
return;
}
const editor = widget.inputEditor;
const model = editor.getModel();
if (!model) {
return;
}
const range = (editor.getSelection() || model.getFullModelRange()).collapseToEnd();
const text = mcpPromptPrefix(server.definition) + '.';
model.applyEdits([{ range, text }]);
editor.setSelection(Range.fromPositions(range.getEndPosition().delta(0, text.length)));
widget.focusInput();
SuggestController.get(editor)?.triggerSuggest();
}
}
@@ -178,7 +178,8 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
const range = Range.fromPositions(model.getPositionAt(node.children[0].offset));
const canDebug = !!server.readDefinitions().get().server?.devMode?.debug;
switch (read(server.connectionState).state) {
const state = read(server.connectionState).state;
switch (state) {
case McpConnectionState.Kind.Error:
lenses.lenses.push({
range,
@@ -256,15 +257,8 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
},
});
}
lenses.lenses.push({
range,
command: {
id: '',
title: localize('server.toolCount', '{0} tools', read(server.tools).length),
},
});
break;
case McpConnectionState.Kind.Stopped: {
case McpConnectionState.Kind.Stopped:
lenses.lenses.push({
range,
command: {
@@ -283,17 +277,42 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
},
});
}
const toolCount = read(server.tools).length;
if (toolCount) {
lenses.lenses.push({
range,
command: {
id: '',
title: localize('server.toolCountCached', '{0} cached tools', toolCount),
}
});
}
}
if (state !== McpConnectionState.Kind.Error) {
const toolCount = read(server.tools).length;
if (toolCount) {
lenses.lenses.push({
range,
command: {
id: '',
title: localize('server.toolCount', '{0} tools', toolCount),
}
});
}
const promptCount = read(server.prompts).length;
if (promptCount) {
lenses.lenses.push({
range,
command: {
id: McpCommandIds.StartPromptForServer,
title: localize('server.promptcount', '{0} prompts', promptCount),
arguments: [server],
}
});
}
lenses.lenses.push({
range,
command: {
id: McpCommandIds.ServerOptions,
title: localize('mcp.server.more', 'More...'),
arguments: [server.definition.id],
}
});
}
}
@@ -21,6 +21,7 @@ export const enum McpCommandIds {
ServerOptions = 'workbench.mcp.serverOptions',
ShowConfiguration = 'workbench.mcp.showConfiguration',
ShowOutput = 'workbench.mcp.showOutput',
StartPromptForServer = 'workbench.mcp.startPromptForServer',
StartServer = 'workbench.mcp.startServer',
StopServer = 'workbench.mcp.stopServer',
}
@@ -30,7 +30,7 @@ import { mcpActivationEvent } from './mcpConfiguration.js';
import { McpDevModeServerAttache } from './mcpDevMode.js';
import { IMcpRegistry } from './mcpRegistryTypes.js';
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
import { extensionMcpCollectionPrefix, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerTransportType, McpToolName } from './mcpTypes.js';
import { extensionMcpCollectionPrefix, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerTransportType, McpToolName } from './mcpTypes.js';
import { MCP } from './modelContextProtocol.js';
import { UriTemplate } from './uriTemplate.js';
@@ -657,7 +657,7 @@ class McpPrompt implements IMcpPrompt {
private readonly _server: McpServer,
private readonly _definition: MCP.Prompt,
) {
this.id = (this._server.definition.label + '.' + _definition.name).replace(/[^a-z0-9_.-]/gi, '_');
this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);
this.name = _definition.name;
this.description = _definition.description;
this.arguments = _definition.arguments || [];
@@ -335,6 +335,11 @@ export interface IMcpPrompt {
resolve(args: Record<string, string | undefined>, token?: CancellationToken): Promise<IMcpPromptMessage[]>;
}
export const mcpPromptReplaceSpecialChars = (s: string) => s.replace(/[^a-z0-9_.-]/gi, '_');
export const mcpPromptPrefix = (definition: McpDefinitionReference) =>
`/mcp.` + mcpPromptReplaceSpecialChars(definition.label);
export interface IMcpPromptMessage extends MCP.PromptMessage { }
export interface IMcpTool {