diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 14eb53bc6a5..81c1845e6a2 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -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; } diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 4f9ced11c89..3a3dab71950 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -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); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 9ab0126220f..15fa21381de 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -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 { + 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(); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts index 37a692d3244..38186082bf0 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -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], + } + }); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index af71441b754..d7ccde3c119 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -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', } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 59dc3f831f0..41b55ce3992 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -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 || []; diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 55687d876d2..f22bc85b778 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -335,6 +335,11 @@ export interface IMcpPrompt { resolve(args: Record, token?: CancellationToken): Promise; } +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 {