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
3296 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, h } 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 { markdownCommandLink, 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 { McpGalleryManifestStatus } from '../../../../platform/mcp/common/mcpGalleryManifest.js';
35
import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
36
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
37
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
38
import { IProductService } from '../../../../platform/product/common/productService.js';
39
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
40
import { StorageScope } from '../../../../platform/storage/common/storage.js';
41
import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
42
import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js';
43
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
44
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';
45
import { ActiveEditorContext, RemoteNameContext, ResourceContextKey, WorkbenchStateContext, WorkspaceFolderCountContext } from '../../../common/contextkeys.js';
46
import { IWorkbenchContribution } from '../../../common/contributions.js';
47
import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';
48
import { IAccountQuery, IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';
49
import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';
50
import { IEditorService } from '../../../services/editor/common/editorService.js';
51
import { IRemoteUserDataProfilesService } from '../../../services/userDataProfile/common/remoteUserDataProfiles.js';
52
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
53
import { IViewsService } from '../../../services/views/common/viewsService.js';
54
import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js';
55
import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js';
56
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
57
import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService.js';
58
import { ChatModeKind } from '../../chat/common/constants.js';
59
import { ILanguageModelsService } from '../../chat/common/languageModels.js';
60
import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js';
61
import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js';
62
import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
63
import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
64
import { McpCommandIds } from '../common/mcpCommandIds.js';
65
import { McpContextKeys } from '../common/mcpContextKeys.js';
66
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
67
import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpServersGalleryStatusContext, McpStartServerInteraction } from '../common/mcpTypes.js';
68
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
69
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
70
import './media/mcpServerAction.css';
71
import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';
72
73
// acroynms do not get localized
74
const category: ILocalizedString = {
75
original: 'MCP',
76
value: 'MCP',
77
};
78
79
export class ListMcpServerCommand extends Action2 {
80
constructor() {
81
super({
82
id: McpCommandIds.ListServer,
83
title: localize2('mcp.list', 'List Servers'),
84
icon: Codicon.server,
85
category,
86
f1: true,
87
menu: [{
88
when: ContextKeyExpr.and(
89
ContextKeyExpr.or(
90
ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools),
91
McpContextKeys.hasServersWithErrors,
92
),
93
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
94
ChatContextKeys.lockedToCodingAgent.negate()
95
),
96
id: MenuId.ChatExecute,
97
group: 'navigation',
98
order: 2,
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
store.add(pick);
115
116
store.add(autorun(reader => {
117
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);
118
const firstRun = pick.items.length === 0;
119
pick.items = [
120
{ 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) },
121
...Object.values(servers).filter(s => s.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [
122
{ type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id },
123
...servers.map(server => ({
124
id: server.definition.id,
125
label: server.definition.label,
126
description: McpConnectionState.toString(server.connectionState.read(reader)),
127
})),
128
]),
129
];
130
131
if (firstRun && pick.items.length > 3) {
132
pick.activeItems = pick.items.slice(2, 3) as ItemType[]; // select the first server by default
133
}
134
}));
135
136
137
const picked = await new Promise<ItemType | undefined>(resolve => {
138
store.add(pick.onDidAccept(() => {
139
resolve(pick.activeItems[0]);
140
}));
141
store.add(pick.onDidHide(() => {
142
resolve(undefined);
143
}));
144
pick.show();
145
});
146
147
store.dispose();
148
149
if (!picked) {
150
// no-op
151
} else if (picked.id === '$add') {
152
commandService.executeCommand(McpCommandIds.AddConfiguration);
153
} else {
154
commandService.executeCommand(McpCommandIds.ServerOptions, picked.id);
155
}
156
}
157
}
158
159
interface ActionItem extends IQuickPickItem {
160
action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config' | 'configSampling' | 'samplingLog' | 'resources';
161
}
162
163
interface AuthActionItem extends IQuickPickItem {
164
action: 'disconnect' | 'signout';
165
accountQuery: IAccountQuery;
166
}
167
168
export class McpConfirmationServerOptionsCommand extends Action2 {
169
constructor() {
170
super({
171
id: McpCommandIds.ServerOptionsInConfirmation,
172
title: localize2('mcp.options', 'Server Options'),
173
category,
174
icon: Codicon.settingsGear,
175
f1: false,
176
menu: [{
177
id: MenuId.ChatConfirmationMenu,
178
when: ContextKeyExpr.and(
179
ContextKeyExpr.equals('chatConfirmationPartSource', 'mcp'),
180
ContextKeyExpr.or(
181
ContextKeyExpr.equals('chatConfirmationPartType', 'chatToolConfirmation'),
182
ContextKeyExpr.equals('chatConfirmationPartType', 'elicitation'),
183
),
184
),
185
group: 'navigation'
186
}],
187
});
188
}
189
190
override async run(accessor: ServicesAccessor, arg: IChatToolInvocation | IChatElicitationRequest): Promise<void> {
191
const toolsService = accessor.get(ILanguageModelToolsService);
192
if (arg.kind === 'toolInvocation') {
193
const tool = toolsService.getTool(arg.toolId);
194
if (tool?.source.type === 'mcp') {
195
accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, tool.source.definitionId);
196
}
197
} else if (arg.kind === 'elicitation') {
198
if (arg.source?.type === 'mcp') {
199
accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, arg.source.definitionId);
200
}
201
} else {
202
assertNever(arg);
203
}
204
}
205
}
206
207
export class McpServerOptionsCommand extends Action2 {
208
constructor() {
209
super({
210
id: McpCommandIds.ServerOptions,
211
title: localize2('mcp.options', 'Server Options'),
212
category,
213
f1: false,
214
});
215
}
216
217
override async run(accessor: ServicesAccessor, id: string): Promise<void> {
218
const mcpService = accessor.get(IMcpService);
219
const quickInputService = accessor.get(IQuickInputService);
220
const mcpRegistry = accessor.get(IMcpRegistry);
221
const editorService = accessor.get(IEditorService);
222
const commandService = accessor.get(ICommandService);
223
const samplingService = accessor.get(IMcpSamplingService);
224
const authenticationQueryService = accessor.get(IAuthenticationQueryService);
225
const authenticationService = accessor.get(IAuthenticationService);
226
const server = mcpService.servers.get().find(s => s.definition.id === id);
227
if (!server) {
228
return;
229
}
230
231
const collection = mcpRegistry.collections.get().find(c => c.id === server.collection.id);
232
const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id);
233
234
const items: (ActionItem | AuthActionItem | IQuickPickSeparator)[] = [];
235
const serverState = server.connectionState.get();
236
237
items.push({ type: 'separator', label: localize('mcp.actions.status', 'Status') });
238
239
// Only show start when server is stopped or in error state
240
if (McpConnectionState.canBeStarted(serverState.state)) {
241
items.push({
242
label: localize('mcp.start', 'Start Server'),
243
action: 'start'
244
});
245
} else {
246
items.push({
247
label: localize('mcp.stop', 'Stop Server'),
248
action: 'stop'
249
});
250
items.push({
251
label: localize('mcp.restart', 'Restart Server'),
252
action: 'restart'
253
});
254
}
255
256
items.push(...this._getAuthActions(authenticationQueryService, server.definition.id));
257
258
const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin;
259
if (configTarget) {
260
items.push({
261
label: localize('mcp.config', 'Show Configuration'),
262
action: 'config',
263
});
264
}
265
266
items.push({
267
label: localize('mcp.showOutput', 'Show Output'),
268
action: 'showOutput'
269
});
270
271
items.push(
272
{ type: 'separator', label: localize('mcp.actions.sampling', 'Sampling') },
273
{
274
label: localize('mcp.configAccess', 'Configure Model Access'),
275
description: localize('mcp.showOutput.description', 'Set the models the server can use via MCP sampling'),
276
action: 'configSampling'
277
},
278
);
279
280
281
if (samplingService.hasLogs(server)) {
282
items.push({
283
label: localize('mcp.samplingLog', 'Show Sampling Requests'),
284
description: localize('mcp.samplingLog.description', 'Show the sampling requests for this server'),
285
action: 'samplingLog',
286
});
287
}
288
289
const capabilities = server.capabilities.get();
290
if (capabilities === undefined || (capabilities & McpCapability.Resources)) {
291
items.push({ type: 'separator', label: localize('mcp.actions.resources', 'Resources') });
292
items.push({
293
label: localize('mcp.resources', 'Browse Resources'),
294
action: 'resources',
295
});
296
}
297
298
const pick = await quickInputService.pick(items, {
299
placeHolder: localize('mcp.selectAction', 'Select action for \'{0}\'', server.definition.label),
300
});
301
302
if (!pick) {
303
return;
304
}
305
306
switch (pick.action) {
307
case 'start':
308
await server.start({ promptType: 'all-untrusted' });
309
server.showOutput();
310
break;
311
case 'stop':
312
await server.stop();
313
break;
314
case 'restart':
315
await server.stop();
316
await server.start({ promptType: 'all-untrusted' });
317
break;
318
case 'disconnect':
319
await server.stop();
320
await this._handleAuth(authenticationService, pick.accountQuery, server.definition, false);
321
break;
322
case 'signout':
323
await server.stop();
324
await this._handleAuth(authenticationService, pick.accountQuery, server.definition, true);
325
break;
326
case 'showOutput':
327
server.showOutput();
328
break;
329
case 'config':
330
editorService.openEditor({
331
resource: URI.isUri(configTarget) ? configTarget : configTarget!.uri,
332
options: { selection: URI.isUri(configTarget) ? undefined : configTarget!.range }
333
});
334
break;
335
case 'configSampling':
336
return commandService.executeCommand(McpCommandIds.ConfigureSamplingModels, server);
337
case 'resources':
338
return commandService.executeCommand(McpCommandIds.BrowseResources, server);
339
case 'samplingLog':
340
editorService.openEditor({
341
resource: undefined,
342
contents: samplingService.getLogText(server),
343
label: localize('mcp.samplingLog.title', 'MCP Sampling: {0}', server.definition.label),
344
});
345
break;
346
default:
347
assertNever(pick);
348
}
349
}
350
351
private _getAuthActions(
352
authenticationQueryService: IAuthenticationQueryService,
353
serverId: string
354
): AuthActionItem[] {
355
const result: AuthActionItem[] = [];
356
// Really, this should only ever have one entry.
357
for (const [providerId, accountName] of authenticationQueryService.mcpServer(serverId).getAllAccountPreferences()) {
358
359
const accountQuery = authenticationQueryService.provider(providerId).account(accountName);
360
if (!accountQuery.mcpServer(serverId).isAccessAllowed()) {
361
continue; // skip accounts that are not allowed
362
}
363
// If there are multiple allowed servers/extensions, other things are using this provider
364
// so we show a disconnect action, otherwise we show a sign out action.
365
if (accountQuery.entities().getEntityCount().total > 1) {
366
result.push({
367
action: 'disconnect',
368
label: localize('mcp.disconnect', 'Disconnect Account'),
369
description: `(${accountName})`,
370
accountQuery
371
});
372
} else {
373
result.push({
374
action: 'signout',
375
label: localize('mcp.signOut', 'Sign Out'),
376
description: `(${accountName})`,
377
accountQuery
378
});
379
}
380
}
381
return result;
382
}
383
384
private async _handleAuth(
385
authenticationService: IAuthenticationService,
386
accountQuery: IAccountQuery,
387
definition: McpDefinitionReference,
388
signOut: boolean
389
) {
390
const { providerId, accountName } = accountQuery;
391
accountQuery.mcpServer(definition.id).setAccessAllowed(false, definition.label);
392
if (signOut) {
393
const accounts = await authenticationService.getAccounts(providerId);
394
const account = accounts.find(a => a.label === accountName);
395
if (account) {
396
const sessions = await authenticationService.getSessions(providerId, undefined, { account });
397
for (const session of sessions) {
398
await authenticationService.removeSession(providerId, session.id);
399
}
400
}
401
}
402
}
403
}
404
405
export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution {
406
constructor(
407
@IActionViewItemService actionViewItemService: IActionViewItemService,
408
@IMcpService mcpService: IMcpService,
409
@IInstantiationService instaService: IInstantiationService,
410
@ICommandService commandService: ICommandService,
411
@IConfigurationService configurationService: IConfigurationService,
412
) {
413
super();
414
415
const hoverIsOpen = observableValue(this, false);
416
const config = observableConfigValue(mcpAutoStartConfig, McpAutoStartValue.NewAndOutdated, configurationService);
417
418
const enum DisplayedState {
419
None,
420
NewTools,
421
Error,
422
Refreshing,
423
}
424
425
type DisplayedStateT = {
426
state: DisplayedState;
427
servers: (IMcpServer | McpCollectionDefinition)[];
428
};
429
430
function isServer(s: IMcpServer | McpCollectionDefinition): s is IMcpServer {
431
return typeof (s as IMcpServer).start === 'function';
432
}
433
434
const displayedStateCurrent = derived((reader): DisplayedStateT => {
435
const servers = mcpService.servers.read(reader);
436
const serversPerState: (IMcpServer | McpCollectionDefinition)[][] = [];
437
for (const server of servers) {
438
let thisState = DisplayedState.None;
439
switch (server.cacheState.read(reader)) {
440
case McpServerCacheState.Unknown:
441
case McpServerCacheState.Outdated:
442
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools;
443
break;
444
case McpServerCacheState.RefreshingFromUnknown:
445
thisState = DisplayedState.Refreshing;
446
break;
447
default:
448
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None;
449
break;
450
}
451
452
serversPerState[thisState] ??= [];
453
serversPerState[thisState].push(server);
454
}
455
456
const unknownServerStates = mcpService.lazyCollectionState.read(reader);
457
if (unknownServerStates.state === LazyCollectionState.LoadingUnknown) {
458
serversPerState[DisplayedState.Refreshing] ??= [];
459
serversPerState[DisplayedState.Refreshing].push(...unknownServerStates.collections);
460
} else if (unknownServerStates.state === LazyCollectionState.HasUnknown) {
461
serversPerState[DisplayedState.NewTools] ??= [];
462
serversPerState[DisplayedState.NewTools].push(...unknownServerStates.collections);
463
}
464
465
let maxState = (serversPerState.length - 1) as DisplayedState;
466
if (maxState === DisplayedState.NewTools && config.read(reader) !== McpAutoStartValue.Never) {
467
maxState = DisplayedState.None;
468
}
469
470
return { state: maxState, servers: serversPerState[maxState] || [] };
471
});
472
473
// avoid hiding the hover if a state changes while it's open:
474
const displayedState = derivedObservableWithCache<DisplayedStateT>(this, (reader, last) => {
475
if (last && hoverIsOpen.read(reader)) {
476
return last;
477
} else {
478
return displayedStateCurrent.read(reader);
479
}
480
});
481
482
this._store.add(actionViewItemService.register(MenuId.ChatExecute, McpCommandIds.ListServer, (action, options) => {
483
if (!(action instanceof MenuItemAction)) {
484
return undefined;
485
}
486
487
return instaService.createInstance(class extends MenuEntryActionViewItem {
488
489
override render(container: HTMLElement): void {
490
491
super.render(container);
492
container.classList.add('chat-mcp');
493
494
const action = h('button.chat-mcp-action', [h('span@icon')]);
495
496
this._register(autorun(r => {
497
const displayed = displayedState.read(r);
498
const { state } = displayed;
499
const { root, icon } = action;
500
this.updateTooltip();
501
container.classList.toggle('chat-mcp-has-action', state !== DisplayedState.None);
502
503
if (!root.parentElement) {
504
container.appendChild(root);
505
}
506
507
root.ariaLabel = this.getLabelForState(displayed);
508
root.className = 'chat-mcp-action';
509
icon.className = '';
510
if (state === DisplayedState.NewTools) {
511
root.classList.add('chat-mcp-action-new');
512
icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.refresh));
513
} else if (state === DisplayedState.Error) {
514
root.classList.add('chat-mcp-action-error');
515
icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning));
516
} else if (state === DisplayedState.Refreshing) {
517
root.classList.add('chat-mcp-action-refreshing');
518
icon.classList.add(...ThemeIcon.asClassNameArray(spinningLoading));
519
} else {
520
root.remove();
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) => markdownCommandLink({
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.fromObservable(displayedState)));
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: McpContextKeys.toolsCount.greater(0),
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: McpContextKeys.toolsCount.greater(0),
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
menu: {
700
id: MenuId.EditorContent,
701
when: ContextKeyExpr.and(
702
ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/),
703
ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID)
704
)
705
}
706
});
707
}
708
709
async run(accessor: ServicesAccessor, configUri?: string): Promise<void> {
710
const instantiationService = accessor.get(IInstantiationService);
711
const workspaceService = accessor.get(IWorkspaceContextService);
712
const target = configUri ? workspaceService.getWorkspaceFolder(URI.parse(configUri)) : undefined;
713
return instantiationService.createInstance(McpAddConfigurationCommand, target ?? undefined).run();
714
}
715
}
716
717
718
export class RemoveStoredInput extends Action2 {
719
constructor() {
720
super({
721
id: McpCommandIds.RemoveStoredInput,
722
title: localize2('mcp.resetCachedTools', "Reset Cached Tools"),
723
category,
724
f1: false,
725
});
726
}
727
728
run(accessor: ServicesAccessor, scope: StorageScope, id?: string): void {
729
accessor.get(IMcpRegistry).clearSavedInputs(scope, id);
730
}
731
}
732
733
export class EditStoredInput extends Action2 {
734
constructor() {
735
super({
736
id: McpCommandIds.EditStoredInput,
737
title: localize2('mcp.editStoredInput', "Edit Stored Input"),
738
category,
739
f1: false,
740
});
741
}
742
743
run(accessor: ServicesAccessor, inputId: string, uri: URI | undefined, configSection: string, target: ConfigurationTarget): void {
744
const workspaceFolder = uri && accessor.get(IWorkspaceContextService).getWorkspaceFolder(uri);
745
accessor.get(IMcpRegistry).editSavedInput(inputId, workspaceFolder || undefined, configSection, target);
746
}
747
}
748
749
export class ShowConfiguration extends Action2 {
750
constructor() {
751
super({
752
id: McpCommandIds.ShowConfiguration,
753
title: localize2('mcp.command.showConfiguration', "Show Configuration"),
754
category,
755
f1: false,
756
});
757
}
758
759
run(accessor: ServicesAccessor, collectionId: string, serverId: string): void {
760
const collection = accessor.get(IMcpRegistry).collections.get().find(c => c.id === collectionId);
761
if (!collection) {
762
return;
763
}
764
765
const server = collection?.serverDefinitions.get().find(s => s.id === serverId);
766
const editorService = accessor.get(IEditorService);
767
if (server?.presentation?.origin) {
768
editorService.openEditor({
769
resource: server.presentation.origin.uri,
770
options: { selection: server.presentation.origin.range }
771
});
772
} else if (collection.presentation?.origin) {
773
editorService.openEditor({
774
resource: collection.presentation.origin,
775
});
776
}
777
}
778
}
779
780
export class ShowOutput extends Action2 {
781
constructor() {
782
super({
783
id: McpCommandIds.ShowOutput,
784
title: localize2('mcp.command.showOutput', "Show Output"),
785
category,
786
f1: false,
787
});
788
}
789
790
run(accessor: ServicesAccessor, serverId: string): void {
791
accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId)?.showOutput();
792
}
793
}
794
795
export class RestartServer extends Action2 {
796
constructor() {
797
super({
798
id: McpCommandIds.RestartServer,
799
title: localize2('mcp.command.restartServer', "Restart Server"),
800
category,
801
f1: false,
802
});
803
}
804
805
async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {
806
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
807
s?.showOutput();
808
await s?.stop();
809
await s?.start({ promptType: 'all-untrusted', ...opts });
810
}
811
}
812
813
export class StartServer extends Action2 {
814
constructor() {
815
super({
816
id: McpCommandIds.StartServer,
817
title: localize2('mcp.command.startServer', "Start Server"),
818
category,
819
f1: false,
820
});
821
}
822
823
async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) {
824
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
825
await s?.start({ promptType: 'all-untrusted', ...opts });
826
}
827
}
828
829
export class StopServer extends Action2 {
830
constructor() {
831
super({
832
id: McpCommandIds.StopServer,
833
title: localize2('mcp.command.stopServer', "Stop Server"),
834
category,
835
f1: false,
836
});
837
}
838
839
async run(accessor: ServicesAccessor, serverId: string) {
840
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
841
await s?.stop();
842
}
843
}
844
845
export class McpBrowseCommand extends Action2 {
846
constructor() {
847
super({
848
id: McpCommandIds.Browse,
849
title: localize2('mcp.command.browse', "MCP Servers"),
850
category,
851
menu: [{
852
id: extensionsFilterSubMenu,
853
group: '1_predefined',
854
order: 1,
855
}],
856
});
857
}
858
859
async run(accessor: ServicesAccessor) {
860
accessor.get(IExtensionsWorkbenchService).openSearch('@mcp ');
861
}
862
}
863
864
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
865
command: {
866
id: McpCommandIds.Browse,
867
title: localize2('mcp.command.browse.mcp', "Browse Servers"),
868
category
869
},
870
});
871
872
export class BrowseMcpServersPageCommand extends Action2 {
873
constructor() {
874
super({
875
id: McpCommandIds.BrowsePage,
876
title: localize2('mcp.command.open', "Browse MCP Servers"),
877
icon: Codicon.globe,
878
menu: [{
879
id: MenuId.ViewTitle,
880
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', InstalledMcpServersViewId), McpServersGalleryStatusContext.isEqualTo(McpGalleryManifestStatus.Unavailable)),
881
group: 'navigation',
882
}],
883
});
884
}
885
886
async run(accessor: ServicesAccessor) {
887
const productService = accessor.get(IProductService);
888
const openerService = accessor.get(IOpenerService);
889
return openerService.open(productService.quality === 'insider' ? 'https://code.visualstudio.com/insider/mcp' : 'https://code.visualstudio.com/mcp');
890
}
891
}
892
893
export class ShowInstalledMcpServersCommand extends Action2 {
894
constructor() {
895
super({
896
id: McpCommandIds.ShowInstalled,
897
title: localize2('mcp.command.show.installed', "Show Installed Servers"),
898
category,
899
precondition: HasInstalledMcpServersContext,
900
f1: true,
901
});
902
}
903
904
async run(accessor: ServicesAccessor) {
905
const viewsService = accessor.get(IViewsService);
906
const view = await viewsService.openView(InstalledMcpServersViewId, true);
907
if (!view) {
908
await viewsService.openViewContainer(VIEW_CONTAINER.id);
909
await viewsService.openView(InstalledMcpServersViewId, true);
910
}
911
}
912
}
913
914
MenuRegistry.appendMenuItem(CHAT_CONFIG_MENU_ID, {
915
command: {
916
id: McpCommandIds.ShowInstalled,
917
title: localize2('mcp.servers', "MCP Servers")
918
},
919
when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
920
order: 14,
921
group: '0_level'
922
});
923
924
abstract class OpenMcpResourceCommand extends Action2 {
925
protected abstract getURI(accessor: ServicesAccessor): Promise<URI>;
926
927
async run(accessor: ServicesAccessor) {
928
const fileService = accessor.get(IFileService);
929
const editorService = accessor.get(IEditorService);
930
const resource = await this.getURI(accessor);
931
if (!(await fileService.exists(resource))) {
932
await fileService.createFile(resource, VSBuffer.fromString(JSON.stringify({ servers: {} }, null, '\t')));
933
}
934
await editorService.openEditor({ resource });
935
}
936
}
937
938
export class OpenUserMcpResourceCommand extends OpenMcpResourceCommand {
939
constructor() {
940
super({
941
id: McpCommandIds.OpenUserMcp,
942
title: localize2('mcp.command.openUserMcp', "Open User Configuration"),
943
category,
944
f1: true
945
});
946
}
947
948
protected override getURI(accessor: ServicesAccessor): Promise<URI> {
949
const userDataProfileService = accessor.get(IUserDataProfileService);
950
return Promise.resolve(userDataProfileService.currentProfile.mcpResource);
951
}
952
}
953
954
export class OpenRemoteUserMcpResourceCommand extends OpenMcpResourceCommand {
955
constructor() {
956
super({
957
id: McpCommandIds.OpenRemoteUserMcp,
958
title: localize2('mcp.command.openRemoteUserMcp', "Open Remote User Configuration"),
959
category,
960
f1: true,
961
precondition: RemoteNameContext.notEqualsTo('')
962
});
963
}
964
965
protected override async getURI(accessor: ServicesAccessor): Promise<URI> {
966
const userDataProfileService = accessor.get(IUserDataProfileService);
967
const remoteUserDataProfileService = accessor.get(IRemoteUserDataProfilesService);
968
const remoteProfile = await remoteUserDataProfileService.getRemoteProfile(userDataProfileService.currentProfile);
969
return remoteProfile.mcpResource;
970
}
971
}
972
973
export class OpenWorkspaceFolderMcpResourceCommand extends Action2 {
974
constructor() {
975
super({
976
id: McpCommandIds.OpenWorkspaceFolderMcp,
977
title: localize2('mcp.command.openWorkspaceFolderMcp', "Open Workspace Folder MCP Configuration"),
978
category,
979
f1: true,
980
precondition: WorkspaceFolderCountContext.notEqualsTo(0)
981
});
982
}
983
984
async run(accessor: ServicesAccessor) {
985
const workspaceContextService = accessor.get(IWorkspaceContextService);
986
const commandService = accessor.get(ICommandService);
987
const editorService = accessor.get(IEditorService);
988
const workspaceFolders = workspaceContextService.getWorkspace().folders;
989
const workspaceFolder = workspaceFolders.length === 1 ? workspaceFolders[0] : await commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);
990
if (workspaceFolder) {
991
await editorService.openEditor({ resource: workspaceFolder.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]) });
992
}
993
}
994
}
995
996
export class OpenWorkspaceMcpResourceCommand extends Action2 {
997
constructor() {
998
super({
999
id: McpCommandIds.OpenWorkspaceMcp,
1000
title: localize2('mcp.command.openWorkspaceMcp', "Open Workspace MCP Configuration"),
1001
category,
1002
f1: true,
1003
precondition: WorkbenchStateContext.isEqualTo('workspace')
1004
});
1005
}
1006
1007
async run(accessor: ServicesAccessor) {
1008
const workspaceContextService = accessor.get(IWorkspaceContextService);
1009
const editorService = accessor.get(IEditorService);
1010
const workspaceConfiguration = workspaceContextService.getWorkspace().configuration;
1011
if (workspaceConfiguration) {
1012
await editorService.openEditor({ resource: workspaceConfiguration });
1013
}
1014
}
1015
}
1016
1017
export class McpBrowseResourcesCommand extends Action2 {
1018
constructor() {
1019
super({
1020
id: McpCommandIds.BrowseResources,
1021
title: localize2('mcp.browseResources', "Browse Resources..."),
1022
category,
1023
precondition: McpContextKeys.serverCount.greater(0),
1024
f1: true,
1025
});
1026
}
1027
1028
run(accessor: ServicesAccessor, server?: IMcpServer): void {
1029
if (server) {
1030
accessor.get(IInstantiationService).createInstance(McpResourceQuickPick, server).pick();
1031
} else {
1032
accessor.get(IQuickInputService).quickAccess.show(McpResourceQuickAccess.PREFIX);
1033
}
1034
}
1035
}
1036
1037
export class McpConfigureSamplingModels extends Action2 {
1038
constructor() {
1039
super({
1040
id: McpCommandIds.ConfigureSamplingModels,
1041
title: localize2('mcp.configureSamplingModels', "Configure SamplingModel"),
1042
category,
1043
});
1044
}
1045
1046
async run(accessor: ServicesAccessor, server: IMcpServer): Promise<number> {
1047
const quickInputService = accessor.get(IQuickInputService);
1048
const lmService = accessor.get(ILanguageModelsService);
1049
const mcpSampling = accessor.get(IMcpSamplingService);
1050
1051
const existingIds = new Set(mcpSampling.getConfig(server).allowedModels);
1052
const allItems: IQuickPickItem[] = lmService.getLanguageModelIds().map(id => {
1053
const model = lmService.lookupLanguageModel(id)!;
1054
if (!model.isUserSelectable) {
1055
return undefined;
1056
}
1057
return {
1058
label: model.name,
1059
description: model.tooltip,
1060
id,
1061
picked: existingIds.size ? existingIds.has(id) : model.isDefault,
1062
};
1063
}).filter(isDefined);
1064
1065
allItems.sort((a, b) => (b.picked ? 1 : 0) - (a.picked ? 1 : 0) || a.label.localeCompare(b.label));
1066
1067
// do the quickpick selection
1068
const picked = await quickInputService.pick(allItems, {
1069
placeHolder: localize('mcp.configureSamplingModels.ph', 'Pick the models {0} can access via MCP sampling', server.definition.label),
1070
canPickMany: true,
1071
});
1072
1073
if (picked) {
1074
await mcpSampling.updateConfig(server, c => c.allowedModels = picked.map(p => p.id!));
1075
}
1076
1077
return picked?.length || 0;
1078
}
1079
}
1080
1081
export class McpStartPromptingServerCommand extends Action2 {
1082
constructor() {
1083
super({
1084
id: McpCommandIds.StartPromptForServer,
1085
title: localize2('mcp.startPromptingServer', "Start Prompting Server"),
1086
category,
1087
f1: false,
1088
});
1089
}
1090
1091
async run(accessor: ServicesAccessor, server: IMcpServer): Promise<void> {
1092
const widget = await openPanelChatAndGetWidget(accessor.get(IViewsService), accessor.get(IChatWidgetService));
1093
if (!widget) {
1094
return;
1095
}
1096
1097
const editor = widget.inputEditor;
1098
const model = editor.getModel();
1099
if (!model) {
1100
return;
1101
}
1102
1103
const range = (editor.getSelection() || model.getFullModelRange()).collapseToEnd();
1104
const text = mcpPromptPrefix(server.definition) + '.';
1105
1106
model.applyEdits([{ range, text }]);
1107
editor.setSelection(Range.fromPositions(range.getEndPosition().delta(0, text.length)));
1108
widget.focusInput();
1109
SuggestController.get(editor)?.triggerSuggest();
1110
}
1111
}
1112
1113