Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts
5334 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $, addDisposableListener, disposableWindowInterval, EventType } from '../../../../base/browser/dom.js';
7
import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';
8
import { IManagedHoverTooltipHTMLElement } from '../../../../base/browser/ui/hover/hover.js';
9
import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js';
10
import { mainWindow } from '../../../../base/browser/window.js';
11
import { findLast } from '../../../../base/common/arraysFind.js';
12
import { assertNever } from '../../../../base/common/assert.js';
13
import { VSBuffer } from '../../../../base/common/buffer.js';
14
import { Codicon } from '../../../../base/common/codicons.js';
15
import { groupBy } from '../../../../base/common/collections.js';
16
import { Event } from '../../../../base/common/event.js';
17
import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js';
18
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
19
import { autorun, derived, derivedObservableWithCache, observableValue } from '../../../../base/common/observable.js';
20
import { ThemeIcon } from '../../../../base/common/themables.js';
21
import { isDefined } from '../../../../base/common/types.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { Range } from '../../../../editor/common/core/range.js';
24
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
25
import { ILocalizedString, localize, localize2 } from '../../../../nls.js';
26
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
27
import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
28
import { Action2, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js';
29
import { ICommandService } from '../../../../platform/commands/common/commands.js';
30
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
31
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
32
import { IFileService } from '../../../../platform/files/common/files.js';
33
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
34
import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
35
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
36
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
37
import { StorageScope } from '../../../../platform/storage/common/storage.js';
38
import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
39
import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js';
40
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
41
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';
42
import { ActiveEditorContext, RemoteNameContext, ResourceContextKey, WorkbenchStateContext, WorkspaceFolderCountContext } from '../../../common/contextkeys.js';
43
import { IWorkbenchContribution } from '../../../common/contributions.js';
44
import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';
45
import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';
46
import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';
47
import { IEditorService } from '../../../services/editor/common/editorService.js';
48
import { IRemoteUserDataProfilesService } from '../../../services/userDataProfile/common/remoteUserDataProfiles.js';
49
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
50
import { IViewsService } from '../../../services/views/common/viewsService.js';
51
import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js';
52
import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js';
53
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
54
import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService/chatService.js';
55
import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js';
56
import { ILanguageModelsService } from '../../chat/common/languageModels.js';
57
import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js';
58
import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';
59
import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
60
import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
61
import { McpCommandIds } from '../common/mcpCommandIds.js';
62
import { McpContextKeys } from '../common/mcpContextKeys.js';
63
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
64
import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js';
65
import { McpAddConfigurationCommand, McpInstallFromManifestCommand } from './mcpCommandsAddConfiguration.js';
66
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
67
import { startServerAndWaitForLiveTools } from '../common/mcpTypesUtils.js';
68
import './media/mcpServerAction.css';
69
import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';
70
71
// acroynms do not get localized
72
const category: ILocalizedString = {
73
original: 'MCP',
74
value: 'MCP',
75
};
76
77
export class ListMcpServerCommand extends Action2 {
78
constructor() {
79
super({
80
id: McpCommandIds.ListServer,
81
title: localize2('mcp.list', 'List Servers'),
82
icon: Codicon.server,
83
category,
84
f1: true,
85
precondition: ChatContextKeys.Setup.hidden.negate(),
86
menu: [{
87
when: ContextKeyExpr.and(
88
ContextKeyExpr.or(
89
ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools),
90
McpContextKeys.hasServersWithErrors,
91
),
92
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
93
ChatContextKeys.lockedToCodingAgent.negate(),
94
ChatContextKeys.Setup.hidden.negate(),
95
),
96
id: MenuId.ChatInput,
97
group: 'navigation',
98
order: 101,
99
}],
100
});
101
}
102
103
override async run(accessor: ServicesAccessor) {
104
const mcpService = accessor.get(IMcpService);
105
const commandService = accessor.get(ICommandService);
106
const quickInput = accessor.get(IQuickInputService);
107
108
type ItemType = { id: string } & IQuickPickItem;
109
110
const store = new DisposableStore();
111
const pick = quickInput.createQuickPick<ItemType>({ useSeparators: true });
112
pick.placeholder = localize('mcp.selectServer', 'Select an MCP Server');
113
114
mcpService.activateCollections();
115
116
store.add(pick);
117
118
store.add(autorun(reader => {
119
const servers = groupBy(mcpService.servers.read(reader).slice().sort((a, b) => (a.collection.presentation?.order || 0) - (b.collection.presentation?.order || 0)), s => s.collection.id);
120
const firstRun = pick.items.length === 0;
121
pick.items = [
122
{ 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) },
123
...Object.values(servers).filter(s => s!.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [
124
{ type: 'separator', label: servers![0].collection.label, id: servers![0].collection.id },
125
...servers!.map(server => ({
126
id: server.definition.id,
127
label: server.definition.label,
128
description: McpConnectionState.toString(server.connectionState.read(reader)),
129
})),
130
]),
131
];
132
133
if (firstRun && pick.items.length > 3) {
134
pick.activeItems = pick.items.slice(2, 3) as ItemType[]; // select the first server by default
135
}
136
}));
137
138
139
const picked = await new Promise<ItemType | undefined>(resolve => {
140
store.add(pick.onDidAccept(() => {
141
resolve(pick.activeItems[0]);
142
}));
143
store.add(pick.onDidHide(() => {
144
resolve(undefined);
145
}));
146
pick.show();
147
});
148
149
store.dispose();
150
151
if (!picked) {
152
// no-op
153
} else if (picked.id === '$add') {
154
commandService.executeCommand(McpCommandIds.AddConfiguration);
155
} else {
156
commandService.executeCommand(McpCommandIds.ServerOptions, picked.id);
157
}
158
}
159
}
160
161
interface ActionItem extends IQuickPickItem {
162
action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config' | 'configSampling' | 'samplingLog' | 'resources';
163
}
164
165
interface AuthActionItem extends IQuickPickItem {
166
action: 'disconnect' | 'signout';
167
accountQuery: IAccountQuery;
168
}
169
170
export class McpConfirmationServerOptionsCommand extends Action2 {
171
constructor() {
172
super({
173
id: McpCommandIds.ServerOptionsInConfirmation,
174
title: localize2('mcp.options', 'Server Options'),
175
category,
176
icon: Codicon.settingsGear,
177
f1: false,
178
menu: [{
179
id: MenuId.ChatConfirmationMenu,
180
when: ContextKeyExpr.and(
181
ContextKeyExpr.equals('chatConfirmationPartSource', 'mcp'),
182
ContextKeyExpr.or(
183
ContextKeyExpr.equals('chatConfirmationPartType', 'chatToolConfirmation'),
184
ContextKeyExpr.equals('chatConfirmationPartType', 'elicitation'),
185
),
186
),
187
group: 'navigation'
188
}],
189
});
190
}
191
192
override async run(accessor: ServicesAccessor, arg: IChatToolInvocation | IChatElicitationRequest): Promise<void> {
193
const toolsService = accessor.get(ILanguageModelToolsService);
194
if (arg.kind === 'toolInvocation') {
195
const tool = toolsService.getTool(arg.toolId);
196
if (tool?.source.type === 'mcp') {
197
accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, tool.source.definitionId);
198
}
199
} else if (arg.kind === 'elicitation2') {
200
if (arg.source?.type === 'mcp') {
201
accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, arg.source.definitionId);
202
}
203
} else {
204
assertNever(arg);
205
}
206
}
207
}
208
209
export class McpServerOptionsCommand extends Action2 {
210
constructor() {
211
super({
212
id: McpCommandIds.ServerOptions,
213
title: localize2('mcp.options', 'Server Options'),
214
category,
215
f1: false,
216
});
217
}
218
219
override async run(accessor: ServicesAccessor, id: string): Promise<void> {
220
const mcpService = accessor.get(IMcpService);
221
const quickInputService = accessor.get(IQuickInputService);
222
const mcpRegistry = accessor.get(IMcpRegistry);
223
const editorService = accessor.get(IEditorService);
224
const commandService = accessor.get(ICommandService);
225
const samplingService = accessor.get(IMcpSamplingService);
226
const authenticationQueryService = accessor.get(IAuthenticationQueryService);
227
const authenticationService = accessor.get(IAuthenticationService);
228
const server = mcpService.servers.get().find(s => s.definition.id === id);
229
if (!server) {
230
return;
231
}
232
233
const collection = mcpRegistry.collections.get().find(c => c.id === server.collection.id);
234
const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);
235
236
const items: (ActionItem | AuthActionItem | IQuickPickSeparator)[] = [];
237
const serverState = server.connectionState.get();
238
239
items.push({ type: 'separator', label: localize('mcp.actions.status', 'Status') });
240
241
// Only show start when server is stopped or in error state
242
if (McpConnectionState.canBeStarted(serverState.state)) {
243
items.push({
244
label: localize('mcp.start', 'Start Server'),
245
action: 'start'
246
});
247
} else {
248
items.push({
249
label: localize('mcp.stop', 'Stop Server'),
250
action: 'stop'
251
});
252
items.push({
253
label: localize('mcp.restart', 'Restart Server'),
254
action: 'restart'
255
});
256
}
257
258
items.push(...this._getAuthActions(authenticationQueryService, server.definition.id));
259
260
const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;
261
if (configTarget) {
262
items.push({
263
label: localize('mcp.config', 'Show Configuration'),
264
action: 'config',
265
});
266
}
267
268
items.push({
269
label: localize('mcp.showOutput', 'Show Output'),
270
action: 'showOutput'
271
});
272
273
items.push(
274
{ type: 'separator', label: localize('mcp.actions.sampling', 'Sampling') },
275
{
276
label: localize('mcp.configAccess', 'Configure Model Access'),
277
description: localize('mcp.showOutput.description', 'Set the models the server can use via MCP sampling'),
278
action: 'configSampling'
279
},
280
);
281
282
283
if (samplingService.hasLogs(server)) {
284
items.push({
285
label: localize('mcp.samplingLog', 'Show Sampling Requests'),
286
description: localize('mcp.samplingLog.description', 'Show the sampling requests for this server'),
287
action: 'samplingLog',
288
});
289
}
290
291
const capabilities = server.capabilities.get();
292
if (capabilities === undefined || (capabilities & McpCapability.Resources)) {
293
items.push({ type: 'separator', label: localize('mcp.actions.resources', 'Resources') });
294
items.push({
295
label: localize('mcp.resources', 'Browse Resources'),
296
action: 'resources',
297
});
298
}
299
300
const pick = await quickInputService.pick(items, {
301
placeHolder: localize('mcp.selectAction', 'Select action for \'{0}\'', server.definition.label),
302
});
303
304
if (!pick) {
305
return;
306
}
307
308
switch (pick.action) {
309
case 'start':
310
await server.start({ promptType: 'all-untrusted' });
311
server.showOutput();
312
break;
313
case 'stop':
314
await server.stop();
315
break;
316
case 'restart':
317
await server.stop();
318
await server.start({ promptType: 'all-untrusted' });
319
break;
320
case 'disconnect':
321
await server.stop();
322
await this._handleAuth(authenticationService, pick.accountQuery, server.definition, false);
323
break;
324
case 'signout':
325
await server.stop();
326
await this._handleAuth(authenticationService, pick.accountQuery, server.definition, true);
327
break;
328
case 'showOutput':
329
server.showOutput();
330
break;
331
case 'config':
332
editorService.openEditor({
333
resource: URI.isUri(configTarget) ? configTarget : configTarget!.uri,
334
options: { selection: URI.isUri(configTarget) ? undefined : configTarget!.range }
335
});
336
break;
337
case 'configSampling':
338
return commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);
339
case 'resources':
340
return commandService.executeCommand(McpCommandIds.BrowseResources, server);
341
case 'samplingLog':
342
editorService.openEditor({
343
resource: undefined,
344
contents: samplingService.getLogText(server),
345
label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),
346
});
347
break;
348
default:
349
assertNever(pick);
350
}
351
}
352
353
private _getAuthActions(
354
authenticationQueryService: IAuthenticationQueryService,
355
serverId: string
356
): AuthActionItem[] {
357
const result: AuthActionItem[] = [];
358
// Really, this should only ever have one entry.
359
for (const [providerId, accountName] of authenticationQueryService.mcpServer(serverId).getAllAccountPreferences()) {
360
361
const accountQuery = authenticationQueryService.provider(providerId).account(accountName);
362
if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {
363
continue; // skip accounts that are not allowed
364
}
365
// If there are multiple allowed servers/extensions, other things are using this provider
366
// so we show a disconnect action, otherwise we show a sign out action.
367
if (accountQuery.entities().getEntityCount().total > 1) {
368
result.push({
369
action: 'disconnect',
370
label: localize('mcp.disconnect', 'Disconnect Account'),
371
description: `(${accountName})`,
372
accountQuery
373
});
374
} else {
375
result.push({
376
action: 'signout',
377
label: localize('mcp.signOut', 'Sign Out'),
378
description: `(${accountName})`,
379
accountQuery
380
});
381
}
382
}
383
return result;
384
}
385
386
private async _handleAuth(
387
authenticationService: IAuthenticationService,
388
accountQuery: IAccountQuery,
389
definition: McpDefinitionReference,
390
signOut: boolean
391
) {
392
const { providerId, accountName } = accountQuery;
393
accountQuery.mcpServer(definition.id).setAccessAllowed(false, definition.label);
394
if (signOut) {
395
const accounts = await authenticationService.getAccounts(providerId);
396
const account = accounts.find(a => a.label === accountName);
397
if (account) {
398
const sessions = await authenticationService.getSessions(providerId, undefined, { account });
399
for (const session of sessions) {
400
await authenticationService.removeSession(providerId, session.id);
401
}
402
}
403
}
404
}
405
}
406
407
export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution {
408
constructor(
409
@IActionViewItemService actionViewItemService: IActionViewItemService,
410
@IMcpService mcpService: IMcpService,
411
@IInstantiationService instaService: IInstantiationService,
412
@ICommandService commandService: ICommandService,
413
@IConfigurationService configurationService: IConfigurationService,
414
) {
415
super();
416
417
const hoverIsOpen = observableValue(this, false);
418
const config = observableConfigValue(mcpAutoStartConfig, McpAutoStartValue.NewAndOutdated, configurationService);
419
420
const enum DisplayedState {
421
None,
422
NewTools,
423
Error,
424
Refreshing,
425
}
426
427
type DisplayedStateT = {
428
state: DisplayedState;
429
servers: (IMcpServer | McpCollectionDefinition)[];
430
};
431
432
function isServer(s: IMcpServer | McpCollectionDefinition): s is IMcpServer {
433
return typeof (s as IMcpServer).start === 'function';
434
}
435
436
const displayedStateCurrent = derived((reader): DisplayedStateT => {
437
const servers = mcpService.servers.read(reader);
438
const serversPerState: (IMcpServer | McpCollectionDefinition)[][] = [];
439
for (const server of servers) {
440
let thisState = DisplayedState.None;
441
switch (server.cacheState.read(reader)) {
442
case McpServerCacheState.Unknown:
443
case McpServerCacheState.Outdated:
444
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools;
445
break;
446
case McpServerCacheState.RefreshingFromUnknown:
447
thisState = DisplayedState.Refreshing;
448
break;
449
default:
450
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None;
451
break;
452
}
453
454
serversPerState[thisState] ??= [];
455
serversPerState[thisState].push(server);
456
}
457
458
const unknownServerStates = mcpService.lazyCollectionState.read(reader);
459
if (unknownServerStates.state === LazyCollectionState.LoadingUnknown) {
460
serversPerState[DisplayedState.Refreshing] ??= [];
461
serversPerState[DisplayedState.Refreshing].push(...unknownServerStates.collections);
462
} else if (unknownServerStates.state === LazyCollectionState.HasUnknown) {
463
serversPerState[DisplayedState.NewTools] ??= [];
464
serversPerState[DisplayedState.NewTools].push(...unknownServerStates.collections);
465
}
466
467
let maxState = (serversPerState.length - 1) as DisplayedState;
468
if (maxState === DisplayedState.NewTools && config.read(reader) !== McpAutoStartValue.Never) {
469
maxState = DisplayedState.None;
470
}
471
472
return { state: maxState, servers: serversPerState[maxState] || [] };
473
});
474
475
// avoid hiding the hover if a state changes while it's open:
476
const displayedState = derivedObservableWithCache<DisplayedStateT>(this, (reader, last) => {
477
if (last && hoverIsOpen.read(reader)) {
478
return last;
479
} else {
480
return displayedStateCurrent.read(reader);
481
}
482
});
483
484
const actionItemState = displayedState.map(s => s.state);
485
486
this._store.add(actionViewItemService.register(MenuId.ChatInput, McpCommandIds.ListServer, (action, options) => {
487
if (!(action instanceof MenuItemAction)) {
488
return undefined;
489
}
490
491
return instaService.createInstance(class extends MenuEntryActionViewItem {
492
493
override render(container: HTMLElement): void {
494
495
super.render(container);
496
container.classList.add('chat-mcp');
497
container.style.position = 'relative';
498
499
const stateIndicator = container.appendChild($('.chat-mcp-state-indicator'));
500
stateIndicator.style.display = 'none';
501
502
this._register(autorun(r => {
503
const displayed = displayedState.read(r);
504
const { state } = displayed;
505
this.updateTooltip();
506
507
508
stateIndicator.ariaLabel = this.getLabelForState(displayed);
509
stateIndicator.className = 'chat-mcp-state-indicator';
510
if (state === DisplayedState.NewTools) {
511
stateIndicator.style.display = 'block';
512
stateIndicator.classList.add('chat-mcp-state-new', ...ThemeIcon.asClassNameArray(Codicon.refresh));
513
} else if (state === DisplayedState.Error) {
514
stateIndicator.style.display = 'block';
515
stateIndicator.classList.add('chat-mcp-state-error', ...ThemeIcon.asClassNameArray(Codicon.warning));
516
} else if (state === DisplayedState.Refreshing) {
517
stateIndicator.style.display = 'block';
518
stateIndicator.classList.add('chat-mcp-state-refreshing', ...ThemeIcon.asClassNameArray(spinningLoading));
519
} else {
520
stateIndicator.style.display = 'none';
521
}
522
}));
523
}
524
525
override async onClick(e: MouseEvent): Promise<void> {
526
e.preventDefault();
527
e.stopPropagation();
528
529
const { state, servers } = displayedStateCurrent.get();
530
if (state === DisplayedState.NewTools) {
531
const interaction = new McpStartServerInteraction();
532
servers.filter(isServer).forEach(server => server.stop().then(() => server.start({ interaction })));
533
mcpService.activateCollections();
534
} else if (state === DisplayedState.Refreshing) {
535
findLast(servers, isServer)?.showOutput();
536
} else if (state === DisplayedState.Error) {
537
const server = findLast(servers, isServer);
538
if (server) {
539
await server.showOutput(true);
540
commandService.executeCommand(McpCommandIds.ServerOptions, server.definition.id);
541
}
542
} else {
543
commandService.executeCommand(McpCommandIds.ListServer);
544
}
545
}
546
547
protected override getTooltip(): string {
548
return this.getLabelForState() || super.getTooltip();
549
}
550
551
protected override getHoverContents({ state, servers } = displayedStateCurrent.get()): string | undefined | IManagedHoverTooltipHTMLElement {
552
const link = (s: IMcpServer) => createMarkdownCommandLink({
553
title: s.definition.label,
554
id: McpCommandIds.ServerOptions,
555
arguments: [s.definition.id],
556
});
557
558
const single = servers.length === 1;
559
const names = servers.map(s => isServer(s) ? link(s) : '`' + s.label + '`').map(l => single ? l : `- ${l}`).join('\n');
560
let markdown: MarkdownString;
561
if (state === DisplayedState.NewTools) {
562
markdown = new MarkdownString(single
563
? localize('mcp.newTools.md.single', "MCP server {0} has been updated and may have new tools available.", names)
564
: localize('mcp.newTools.md.multi', "MCP servers have been updated and may have new tools available:\n\n{0}", names)
565
);
566
} else if (state === DisplayedState.Error) {
567
markdown = new MarkdownString(single
568
? localize('mcp.err.md.single', "MCP server {0} was unable to start successfully.", names)
569
: localize('mcp.err.md.multi', "Multiple MCP servers were unable to start successfully:\n\n{0}", names)
570
);
571
} else {
572
return this.getLabelForState() || undefined;
573
}
574
575
return {
576
element: (token): HTMLElement => {
577
hoverIsOpen.set(true, undefined);
578
579
const store = new DisposableStore();
580
store.add(toDisposable(() => hoverIsOpen.set(false, undefined)));
581
store.add(token.onCancellationRequested(() => {
582
store.dispose();
583
}));
584
585
// todo@connor4312/@benibenj: workaround for #257923
586
store.add(disposableWindowInterval(mainWindow, () => {
587
if (!container.isConnected) {
588
store.dispose();
589
}
590
}, 2000));
591
592
const container = $('div.mcp-hover-contents');
593
594
// Render markdown content
595
markdown.isTrusted = true;
596
const markdownResult = store.add(renderMarkdown(markdown));
597
container.appendChild(markdownResult.element);
598
599
// Add divider
600
const divider = $('hr.mcp-hover-divider');
601
container.appendChild(divider);
602
603
// Add checkbox for mcpAutoStartConfig setting
604
const checkboxContainer = $('div.mcp-hover-setting');
605
const settingLabelStr = localize('mcp.autoStart', "Automatically start MCP servers when sending a chat message");
606
607
const checkbox = store.add(new Checkbox(
608
settingLabelStr,
609
config.get() !== McpAutoStartValue.Never,
610
{ ...defaultCheckboxStyles }
611
));
612
613
checkboxContainer.appendChild(checkbox.domNode);
614
615
// Add label next to checkbox
616
const settingLabel = $('span.mcp-hover-setting-label', undefined, settingLabelStr);
617
checkboxContainer.appendChild(settingLabel);
618
619
const onChange = () => {
620
const newValue = checkbox.checked ? McpAutoStartValue.NewAndOutdated : McpAutoStartValue.Never;
621
configurationService.updateValue(mcpAutoStartConfig, newValue);
622
};
623
624
store.add(checkbox.onChange(onChange));
625
626
store.add(addDisposableListener(settingLabel, EventType.CLICK, () => {
627
checkbox.checked = !checkbox.checked;
628
onChange();
629
}));
630
container.appendChild(checkboxContainer);
631
632
return container;
633
},
634
};
635
}
636
637
private getLabelForState({ state, servers } = displayedStateCurrent.get()) {
638
if (state === DisplayedState.NewTools) {
639
return localize('mcp.newTools', "New tools available ({0})", servers.length || 1);
640
} else if (state === DisplayedState.Error) {
641
return localize('mcp.toolError', "Error loading {0} tool(s)", servers.length || 1);
642
} else if (state === DisplayedState.Refreshing) {
643
return localize('mcp.toolRefresh', "Discovering tools...");
644
} else {
645
return null;
646
}
647
}
648
}, action, { ...options, keybindingNotRenderedWithLabel: true });
649
650
}, Event.fromObservableLight(actionItemState)));
651
}
652
}
653
654
export class ResetMcpTrustCommand extends Action2 {
655
constructor() {
656
super({
657
id: McpCommandIds.ResetTrust,
658
title: localize2('mcp.resetTrust', "Reset Trust"),
659
category,
660
f1: true,
661
precondition: ContextKeyExpr.and(McpContextKeys.toolsCount.greater(0), ChatContextKeys.Setup.hidden.negate()),
662
});
663
}
664
665
run(accessor: ServicesAccessor): void {
666
const mcpService = accessor.get(IMcpService);
667
mcpService.resetTrust();
668
}
669
}
670
671
672
export class ResetMcpCachedTools extends Action2 {
673
constructor() {
674
super({
675
id: McpCommandIds.ResetCachedTools,
676
title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),
677
category,
678
f1: true,
679
precondition: ContextKeyExpr.and(McpContextKeys.toolsCount.greater(0), ChatContextKeys.Setup.hidden.negate()),
680
});
681
}
682
683
run(accessor: ServicesAccessor): void {
684
const mcpService = accessor.get(IMcpService);
685
mcpService.resetCaches();
686
}
687
}
688
689
export class AddConfigurationAction extends Action2 {
690
constructor() {
691
super({
692
id: McpCommandIds.AddConfiguration,
693
title: localize2('mcp.addConfiguration', "Add Server..."),
694
metadata: {
695
description: localize2('mcp.addConfiguration.description', "Installs a new Model Context protocol to the mcp.json settings"),
696
},
697
category,
698
f1: true,
699
precondition: ChatContextKeys.Setup.hidden.negate(),
700
menu: {
701
id: MenuId.EditorContent,
702
when: ContextKeyExpr.and(
703
ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/),
704
ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID),
705
ChatContextKeys.Setup.hidden.negate(),
706
)
707
}
708
});
709
}
710
711
async run(accessor: ServicesAccessor, configUri?: string): Promise<void> {
712
const instantiationService = accessor.get(IInstantiationService);
713
const workspaceService = accessor.get(IWorkspaceContextService);
714
const target = configUri ? workspaceService.getWorkspaceFolder(URI.parse(configUri)) : undefined;
715
return instantiationService.createInstance(McpAddConfigurationCommand, target ?? undefined).run();
716
}
717
}
718
719
export class InstallFromManifestAction extends Action2 {
720
constructor() {
721
super({
722
id: McpCommandIds.InstallFromManifest,
723
title: localize2('mcp.installFromManifest', "Install Server from Manifest..."),
724
metadata: {
725
description: localize2('mcp.installFromManifest.description', "Install an MCP server from a JSON manifest file"),
726
},
727
category,
728
f1: true,
729
precondition: ChatContextKeys.Setup.hidden.negate(),
730
});
731
}
732
733
async run(accessor: ServicesAccessor): Promise<void> {
734
const instantiationService = accessor.get(IInstantiationService);
735
return instantiationService.createInstance(McpInstallFromManifestCommand).run();
736
}
737
}
738
739
740
export class RemoveStoredInput extends Action2 {
741
constructor() {
742
super({
743
id: McpCommandIds.RemoveStoredInput,
744
title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),
745
category,
746
f1: false,
747
});
748
}
749
750
run(accessor: ServicesAccessor, scope: StorageScope, id?: string): void {
751
accessor.get(IMcpRegistry).clearSavedInputs(scope, id);
752
}
753
}
754
755
export class EditStoredInput extends Action2 {
756
constructor() {
757
super({
758
id: McpCommandIds.EditStoredInput,
759
title: localize2('mcp.editStoredInput', "Edit Stored Input"),
760
category,
761
f1: false,
762
});
763
}
764
765
run(accessor: ServicesAccessor, inputId: string, uri: URI | undefined, configSection: string, target: ConfigurationTarget): void {
766
const workspaceFolder = uri && accessor.get(IWorkspaceContextService).getWorkspaceFolder(uri);
767
accessor.get(IMcpRegistry).editSavedInput(inputId, workspaceFolder || undefined, configSection, target);
768
}
769
}
770
771
export class ShowConfiguration extends Action2 {
772
constructor() {
773
super({
774
id: McpCommandIds.ShowConfiguration,
775
title: localize2('mcp.command.showConfiguration', "Show Configuration"),
776
category,
777
f1: false,
778
});
779
}
780
781
run(accessor: ServicesAccessor, collectionId: string, serverId: string): void {
782
const collection = accessor.get(IMcpRegistry).collections.get().find(c => c.id === collectionId);
783
if (!collection) {
784
return;
785
}
786
787
const server = collection?.serverDefinitions.get().find(s => s.id === serverId);
788
const editorService = accessor.get(IEditorService);
789
if (server?.presentation?.origin) {
790
editorService.openEditor({
791
resource: server.presentation.origin.uri,
792
options: { selection: server.presentation.origin.range }
793
});
794
} else if (collection.presentation?.origin) {
795
editorService.openEditor({
796
resource: collection.presentation.origin,
797
});
798
}
799
}
800
}
801
802
export class ShowOutput extends Action2 {
803
constructor() {
804
super({
805
id: McpCommandIds.ShowOutput,
806
title: localize2('mcp.command.showOutput', "Show Output"),
807
category,
808
f1: false,
809
});
810
}
811
812
run(accessor: ServicesAccessor, serverId: string): void {
813
accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId)?.showOutput();
814
}
815
}
816
817
export class RestartServer extends Action2 {
818
constructor() {
819
super({
820
id: McpCommandIds.RestartServer,
821
title: localize2('mcp.command.restartServer', "Restart Server"),
822
category,
823
f1: false,
824
});
825
}
826
827
async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {
828
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
829
s?.showOutput();
830
await s?.stop();
831
await s?.start({ promptType: 'all-untrusted', ...opts });
832
}
833
}
834
835
export class StartServer extends Action2 {
836
constructor() {
837
super({
838
id: McpCommandIds.StartServer,
839
title: localize2('mcp.command.startServer', "Start Server"),
840
category,
841
f1: false,
842
});
843
}
844
845
async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts & { waitForLiveTools?: boolean }) {
846
let servers = accessor.get(IMcpService).servers.get();
847
if (serverId !== '*') {
848
servers = servers.filter(s => s.definition.id === serverId);
849
}
850
851
const startOpts: IMcpServerStartOpts = { promptType: 'all-untrusted', ...opts };
852
if (opts?.waitForLiveTools) {
853
await Promise.all(servers.map(s => startServerAndWaitForLiveTools(s, startOpts)));
854
} else {
855
await Promise.all(servers.map(s => s.start(startOpts)));
856
}
857
}
858
}
859
860
export class StopServer extends Action2 {
861
constructor() {
862
super({
863
id: McpCommandIds.StopServer,
864
title: localize2('mcp.command.stopServer', "Stop Server"),
865
category,
866
f1: false,
867
});
868
}
869
870
async run(accessor: ServicesAccessor, serverId: string) {
871
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
872
await s?.stop();
873
}
874
}
875
876
export class McpBrowseCommand extends Action2 {
877
constructor() {
878
super({
879
id: McpCommandIds.Browse,
880
title: localize2('mcp.command.browse', "MCP Servers"),
881
tooltip: localize2('mcp.command.browse.tooltip', "Browse MCP Servers"),
882
category,
883
icon: Codicon.search,
884
precondition: ChatContextKeys.Setup.hidden.negate(),
885
menu: [{
886
id: extensionsFilterSubMenu,
887
group: '1_predefined',
888
order: 1,
889
when: ChatContextKeys.Setup.hidden.negate(),
890
}, {
891
id: MenuId.ViewTitle,
892
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', InstalledMcpServersViewId), ChatContextKeys.Setup.hidden.negate()),
893
group: 'navigation',
894
}],
895
});
896
}
897
898
async run(accessor: ServicesAccessor) {
899
accessor.get(IExtensionsWorkbenchService).openSearch('@mcp ');
900
}
901
}
902
903
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
904
command: {
905
id: McpCommandIds.Browse,
906
title: localize2('mcp.command.browse.mcp', "Browse MCP Servers"),
907
category,
908
precondition: ChatContextKeys.Setup.hidden.negate(),
909
},
910
});
911
912
export class ShowInstalledMcpServersCommand extends Action2 {
913
constructor() {
914
super({
915
id: McpCommandIds.ShowInstalled,
916
title: localize2('mcp.command.show.installed', "Show Installed Servers"),
917
category,
918
precondition: ContextKeyExpr.and(HasInstalledMcpServersContext, ChatContextKeys.Setup.hidden.negate()),
919
f1: true,
920
});
921
}
922
923
async run(accessor: ServicesAccessor) {
924
const viewsService = accessor.get(IViewsService);
925
const view = await viewsService.openView(InstalledMcpServersViewId, true);
926
if (!view) {
927
await viewsService.openViewContainer(VIEW_CONTAINER.id);
928
await viewsService.openView(InstalledMcpServersViewId, true);
929
}
930
}
931
}
932
933
MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, {
934
command: {
935
id: McpCommandIds.ShowInstalled,
936
title: localize2('mcp.servers', "MCP Servers")
937
},
938
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
939
order: 10,
940
group: '2_level'
941
});
942
943
abstract class OpenMcpResourceCommand extends Action2 {
944
protected abstract getURI(accessor: ServicesAccessor): Promise<URI>;
945
946
async run(accessor: ServicesAccessor) {
947
const fileService = accessor.get(IFileService);
948
const editorService = accessor.get(IEditorService);
949
const resource = await this.getURI(accessor);
950
if (!(await fileService.exists(resource))) {
951
await fileService.createFile(resource, VSBuffer.fromString(JSON.stringify({ servers: {} }, null, '\t')));
952
}
953
await editorService.openEditor({ resource });
954
}
955
}
956
957
export class OpenUserMcpResourceCommand extends OpenMcpResourceCommand {
958
constructor() {
959
super({
960
id: McpCommandIds.OpenUserMcp,
961
title: localize2('mcp.command.openUserMcp', "Open User Configuration"),
962
category,
963
f1: true,
964
precondition: ChatContextKeys.Setup.hidden.negate(),
965
});
966
}
967
968
protected override getURI(accessor: ServicesAccessor): Promise<URI> {
969
const userDataProfileService = accessor.get(IUserDataProfileService);
970
return Promise.resolve(userDataProfileService.currentProfile.mcpResource);
971
}
972
}
973
974
export class OpenRemoteUserMcpResourceCommand extends OpenMcpResourceCommand {
975
constructor() {
976
super({
977
id: McpCommandIds.OpenRemoteUserMcp,
978
title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User Configuration"),
979
category,
980
f1: true,
981
precondition: ContextKeyExpr.and(
982
ChatContextKeys.Setup.hidden.negate(),
983
RemoteNameContext.notEqualsTo('')
984
)
985
});
986
}
987
988
protected override async getURI(accessor: ServicesAccessor): Promise<URI> {
989
const userDataProfileService = accessor.get(IUserDataProfileService);
990
const remoteUserDataProfileService = accessor.get(IRemoteUserDataProfilesService);
991
const remoteProfile = await remoteUserDataProfileService.getRemoteProfile(userDataProfileService.currentProfile);
992
return remoteProfile.mcpResource;
993
}
994
}
995
996
export class OpenWorkspaceFolderMcpResourceCommand extends Action2 {
997
constructor() {
998
super({
999
id: McpCommandIds.OpenWorkspaceFolderMcp,
1000
title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Configuration"),
1001
category,
1002
f1: true,
1003
precondition: ContextKeyExpr.and(
1004
ChatContextKeys.Setup.hidden.negate(),
1005
WorkspaceFolderCountContext.notEqualsTo(0)
1006
)
1007
});
1008
}
1009
1010
async run(accessor: ServicesAccessor) {
1011
const workspaceContextService = accessor.get(IWorkspaceContextService);
1012
const commandService = accessor.get(ICommandService);
1013
const editorService = accessor.get(IEditorService);
1014
const workspaceFolders = workspaceContextService.getWorkspace().folders;
1015
const workspaceFolder = workspaceFolders.length === 1 ? workspaceFolders[0] : await commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);
1016
if (workspaceFolder) {
1017
await editorService.openEditor({ resource: workspaceFolder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]) });
1018
}
1019
}
1020
}
1021
1022
export class OpenWorkspaceMcpResourceCommand extends Action2 {
1023
constructor() {
1024
super({
1025
id: McpCommandIds.OpenWorkspaceMcp,
1026
title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Configuration"),
1027
category,
1028
f1: true,
1029
precondition: ContextKeyExpr.and(
1030
ChatContextKeys.Setup.hidden.negate(),
1031
WorkbenchStateContext.isEqualTo('workspace')
1032
)
1033
});
1034
}
1035
1036
async run(accessor: ServicesAccessor) {
1037
const workspaceContextService = accessor.get(IWorkspaceContextService);
1038
const editorService = accessor.get(IEditorService);
1039
const workspaceConfiguration = workspaceContextService.getWorkspace().configuration;
1040
if (workspaceConfiguration) {
1041
await editorService.openEditor({ resource: workspaceConfiguration });
1042
}
1043
}
1044
}
1045
1046
export class McpBrowseResourcesCommand extends Action2 {
1047
constructor() {
1048
super({
1049
id: McpCommandIds.BrowseResources,
1050
title: localize2('mcp.browseResources', "Browse Resources..."),
1051
category,
1052
precondition: ContextKeyExpr.and(McpContextKeys.serverCount.greater(0), ChatContextKeys.Setup.hidden.negate()),
1053
f1: true,
1054
});
1055
}
1056
1057
run(accessor: ServicesAccessor, server?: IMcpServer): void {
1058
if (server) {
1059
accessor.get(IInstantiationService).createInstance(McpResourceQuickPick, server).pick();
1060
} else {
1061
accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);
1062
}
1063
}
1064
}
1065
1066
export class McpConfigureSamplingModels extends Action2 {
1067
constructor() {
1068
super({
1069
id: McpCommandIds.ConfigureSamplingModels,
1070
title: localize2('mcp.configureSamplingModels', "Configure SamplingModel"),
1071
category,
1072
});
1073
}
1074
1075
async run(accessor: ServicesAccessor, server: IMcpServer): Promise<number> {
1076
const quickInputService = accessor.get(IQuickInputService);
1077
const lmService = accessor.get(ILanguageModelsService);
1078
const mcpSampling = accessor.get(IMcpSamplingService);
1079
1080
const existingIds = new Set(mcpSampling.getConfig(server).allowedModels);
1081
const allItems: IQuickPickItem[] = lmService.getLanguageModelIds().map(id => {
1082
const model = lmService.lookupLanguageModel(id)!;
1083
if (!model.isUserSelectable) {
1084
return undefined;
1085
}
1086
return {
1087
label: model.name,
1088
description: model.tooltip,
1089
id,
1090
picked: existingIds.size ? existingIds.has(id) : model.isDefaultForLocation[ChatAgentLocation.Chat],
1091
};
1092
}).filter(isDefined);
1093
1094
allItems.sort((a, b) => (b.picked ? 1 : 0) - (a.picked ? 1 : 0) || a.label.localeCompare(b.label));
1095
1096
// do the quickpick selection
1097
const picked = await quickInputService.pick(allItems, {
1098
placeHolder: localize('mcp.configureSamplingModels.ph', 'Pick the models {0} can access via MCP sampling', server.definition.label),
1099
canPickMany: true,
1100
});
1101
1102
if (picked) {
1103
await mcpSampling.updateConfig(server, c => c.allowedModels = picked.map(p => p.id!));
1104
}
1105
1106
return picked?.length || 0;
1107
}
1108
}
1109
1110
export class McpStartPromptingServerCommand extends Action2 {
1111
constructor() {
1112
super({
1113
id: McpCommandIds.StartPromptForServer,
1114
title: localize2('mcp.startPromptingServer', "Start Prompting Server"),
1115
category,
1116
f1: false,
1117
});
1118
}
1119
1120
async run(accessor: ServicesAccessor, server: IMcpServer): Promise<void> {
1121
const widget = await openPanelChatAndGetWidget(accessor.get(IViewsService), accessor.get(IChatWidgetService));
1122
if (!widget) {
1123
return;
1124
}
1125
1126
const editor = widget.inputEditor;
1127
const model = editor.getModel();
1128
if (!model) {
1129
return;
1130
}
1131
1132
const range = (editor.getSelection() || model.getFullModelRange()).collapseToEnd();
1133
const text = mcpPromptPrefix(server.definition) + '.';
1134
1135
model.applyEdits([{ range, text }]);
1136
editor.setSelection(Range.fromPositions(range.getEndPosition().delta(0, text.length)));
1137
widget.focusInput();
1138
SuggestController.get(editor)?.triggerSuggest();
1139
}
1140
}
1141
1142
export class McpSkipCurrentAutostartCommand extends Action2 {
1143
constructor() {
1144
super({
1145
id: McpCommandIds.SkipCurrentAutostart,
1146
title: localize2('mcp.skipCurrentAutostart', "Skip Current Autostart"),
1147
category,
1148
f1: false,
1149
});
1150
}
1151
1152
async run(accessor: ServicesAccessor): Promise<void> {
1153
accessor.get(IMcpService).cancelAutostart();
1154
}
1155
}
1156
1157