Skip to content

Commit 4560f08

Browse files
authored
mcp: add status indicator for mcp tools, discovery setting (microsoft#243402)
The tool indicator in chat now has: - A "refresh" action if there are new MCP servers with undiscovered tools (clicking starts the servers) - An error if any tool fails to start (clicking opens the first failing tool options quickpick) - A loading indicator while they're starting (clicking opens output) This also adds a `chat.mpc.discovery.enabled` setting, off by default for now, to configure whether filesystem discovery is enabled. I expect to turn this on as we gain confidence with the feature. Refs microsoft#243229
1 parent 4926127 commit 4560f08

File tree

10 files changed

+213
-20
lines changed

10 files changed

+213
-20
lines changed

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ import { ChatStatusBarEntry } from './chatStatus.js';
101101
import product from '../../../../platform/product/common/product.js';
102102
import { Event } from '../../../../base/common/event.js';
103103
import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditing/notebook/chatEditingNotebookFileSystemProvider.js';
104-
import { mcpConfigurationSection, mcpSchemaExampleServers } from '../../mcp/common/mcpConfiguration.js';
104+
import { mcpConfigurationSection, mcpDiscoverySection, mcpSchemaExampleServers } from '../../mcp/common/mcpConfiguration.js';
105105
import { mcpSchemaId } from '../../../services/configuration/common/configuration.js';
106106
import { ChatTransferService, IChatTransferService } from '../common/chatTransferService.js';
107107
import { ChatTransferContribution } from './actions/chatTransfer.js';
@@ -213,6 +213,11 @@ configurationRegistry.registerConfiguration({
213213
description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),
214214
$ref: mcpSchemaId
215215
},
216+
[mcpDiscoverySection]: {
217+
type: 'boolean',
218+
default: false,
219+
description: nls.localize('mpc.discovery.enabled', "Enable discovery of Model Context Protocol servers on the machine."),
220+
},
216221
[PromptsConfig.CONFIG_KEY]: {
217222
type: 'boolean',
218223
title: nls.localize(

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,45 @@ have to be updated for changes to the rules above, or to support more deeply nes
11491149
gap: 5px;
11501150
}
11511151

1152+
.action-item.chat-mcp {
1153+
display: flex !important;
1154+
1155+
&.chat-mcp-has-action .action-label {
1156+
border-top-right-radius: 0;
1157+
border-bottom-right-radius: 0;
1158+
border-right: 0;
1159+
}
1160+
1161+
.chat-mcp-action {
1162+
align-self: stretch;
1163+
padding: 0 2px;
1164+
border-radius: 0;
1165+
outline: 0;
1166+
border: 0;
1167+
border-top-right-radius: 4px;
1168+
border-bottom-right-radius: 4px;
1169+
background: var(--vscode-button-background);
1170+
cursor: pointer;
1171+
1172+
.codicon {
1173+
width: fit-content;
1174+
color: var(--vscode-button-foreground);
1175+
}
1176+
1177+
.codicon::before {
1178+
font-size: 14px;
1179+
}
1180+
1181+
&.chat-mcp-action-error {
1182+
background: var(--vscode-activityErrorBadge-background);
1183+
1184+
.codicon {
1185+
color: var(--vscode-activityErrorBadge-foreground);
1186+
}
1187+
}
1188+
}
1189+
}
1190+
11521191
.action-item.chat-attached-context-attachment.chat-add-files .action-label.codicon::before {
11531192
font: normal normal normal 16px/1 codicon;
11541193
}

src/vs/workbench/contrib/mcp/browser/mcpCommands.ts

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import './media/mcp.css';
7-
import { reset } from '../../../../base/browser/dom.js';
6+
import { addDisposableListener, EventType, h, reset } from '../../../../base/browser/dom.js';
87
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
8+
import { assertNever } from '../../../../base/common/assert.js';
99
import { Codicon } from '../../../../base/common/codicons.js';
1010
import { diffSets, groupBy } from '../../../../base/common/collections.js';
1111
import { Event } from '../../../../base/common/event.js';
12-
import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js';
12+
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
1313
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
1414
import { autorun, derived, transaction } from '../../../../base/common/observable.js';
15+
import { ThemeIcon } from '../../../../base/common/themables.js';
1516
import { assertType } from '../../../../base/common/types.js';
1617
import { ILocalizedString, localize, localize2 } from '../../../../nls.js';
1718
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
@@ -22,12 +23,14 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
2223
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
2324
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
2425
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
26+
import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js';
2527
import { IWorkbenchContribution } from '../../../common/contributions.js';
2628
import { CHAT_CATEGORY } from '../../chat/browser/actions/chatActions.js';
2729
import { ChatAgentLocation } from '../../chat/common/chatAgents.js';
2830
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
2931
import { McpContextKeys } from '../common/mcpContextKeys.js';
30-
import { IMcpServer, IMcpService, IMcpTool, McpConnectionState } from '../common/mcpTypes.js';
32+
import { IMcpServer, IMcpService, IMcpTool, McpConnectionState, McpServerToolsState } from '../common/mcpTypes.js';
33+
import './media/mcp.css';
3134

3235
// acroynms do not get localized
3336
const category: ILocalizedString = {
@@ -65,7 +68,7 @@ export class ListMcpServerCommand extends Action2 {
6568
...servers.map(server => ({
6669
id: server.definition.id,
6770
label: server.definition.label,
68-
description: McpConnectionState.toString(server.state.read(reader)),
71+
description: McpConnectionState.toString(server.connectionState.read(reader)),
6972
})),
7073
];
7174
});
@@ -117,7 +120,7 @@ export class McpServerOptionsCommand extends Action2 {
117120
}
118121

119122
const items: ActionItem[] = [];
120-
const serverState = server.state.get();
123+
const serverState = server.connectionState.get();
121124

122125
// Only show start when server is stopped or in error state
123126
if (McpConnectionState.canBeStarted(serverState.state)) {
@@ -230,14 +233,14 @@ export class AttachMCPToolsAction extends Action2 {
230233
}
231234
picks.push({
232235
type: 'separator',
233-
label: localize('desc', "MCP Server - {0}", McpConnectionState.toString(server.state.get()))
236+
label: localize('desc', "MCP Server - {0}", McpConnectionState.toString(server.connectionState.get()))
234237
});
235238

236239
const item: ServerPick = {
237240
server,
238241
type: 'item',
239242
label: `${server.definition.label}`,
240-
description: localize('desc', "MCP Server - {0}", McpConnectionState.toString(server.state.get())),
243+
description: localize('desc', "MCP Server - {0}", McpConnectionState.toString(server.connectionState.get())),
241244
picked: tools.some(tool => tool.enabled.get()),
242245
toolPicks: []
243246
};
@@ -340,10 +343,17 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
340343
constructor(
341344
@IActionViewItemService actionViewItemService: IActionViewItemService,
342345
@IMcpService mcpService: IMcpService,
343-
@IInstantiationService instaService: IInstantiationService
346+
@IInstantiationService instaService: IInstantiationService,
347+
@ICommandService commandService: ICommandService,
344348
) {
345349
super();
346350

351+
const enum DisplayedState {
352+
None,
353+
NewTools,
354+
Error,
355+
Refreshing,
356+
}
347357

348358
const toolsCount = derived(r => {
349359
let count = 0;
@@ -358,6 +368,30 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
358368
return { count, enabled };
359369
});
360370

371+
const displayedState = derived(reader => {
372+
const servers = mcpService.servers.read(reader);
373+
const serversPerState: IMcpServer[][] = [];
374+
for (const server of servers) {
375+
let thisState = DisplayedState.None;
376+
switch (server.toolsState.read(reader)) {
377+
case McpServerToolsState.Unknown:
378+
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools;
379+
break;
380+
case McpServerToolsState.RefreshingFromUnknown:
381+
thisState = DisplayedState.Refreshing;
382+
break;
383+
case McpServerToolsState.Cached:
384+
thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None;
385+
break;
386+
}
387+
388+
serversPerState[thisState] ??= [];
389+
serversPerState[thisState].push(server);
390+
}
391+
392+
const maxState = (serversPerState.length - 1) as DisplayedState;
393+
return { state: maxState, servers: serversPerState[maxState] };
394+
});
361395

362396
this._store.add(actionViewItemService.register(MenuId.ChatInputAttachmentToolbar, AttachMCPToolsAction.id, (action, options) => {
363397
if (!(action instanceof MenuItemAction)) {
@@ -371,6 +405,73 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
371405
this.options.label = true;
372406
container.classList.add('chat-mcp');
373407
super.render(container);
408+
409+
const action = h('button.chat-mcp-action', [h('span@icon')]);
410+
411+
this._register(autorun(r => {
412+
const { state, servers } = displayedState.read(r);
413+
const { root, icon } = action;
414+
this.updateTooltip();
415+
container.classList.toggle('chat-mcp-has-action', state !== DisplayedState.None);
416+
417+
if (state === DisplayedState.None) {
418+
root.remove();
419+
return;
420+
}
421+
422+
if (!root.parentElement) {
423+
container.appendChild(root);
424+
}
425+
426+
root.ariaLabel = this.getLabelForState({ state, servers });
427+
root.className = 'chat-mcp-action';
428+
icon.className = '';
429+
if (state === DisplayedState.NewTools) {
430+
root.classList.add('chat-mcp-action-new');
431+
icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.refresh));
432+
} else if (state === DisplayedState.Error) {
433+
root.classList.add('chat-mcp-action-error');
434+
icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning));
435+
} else if (state === DisplayedState.Refreshing) {
436+
root.classList.add('chat-mcp-action-refreshing');
437+
icon.classList.add(...ThemeIcon.asClassNameArray(spinningLoading));
438+
} else {
439+
assertNever(state);
440+
}
441+
}));
442+
443+
this._register(addDisposableListener(action.root, EventType.CLICK, e => {
444+
e.preventDefault();
445+
e.stopPropagation();
446+
447+
const { state, servers } = displayedState.get();
448+
if (state === DisplayedState.NewTools) {
449+
servers.forEach(server => server.start());
450+
} else if (state === DisplayedState.Refreshing) {
451+
servers.at(-1)?.showOutput();
452+
} else if (state === DisplayedState.Error) {
453+
const server = servers.at(-1);
454+
if (server) {
455+
commandService.executeCommand(McpServerOptionsCommand.id, server.definition.id);
456+
}
457+
}
458+
}));
459+
}
460+
461+
protected override getTooltip(): string {
462+
return this.getLabelForState() || super.getTooltip();
463+
}
464+
465+
private getLabelForState({ state, servers } = displayedState.get()) {
466+
if (state === DisplayedState.NewTools) {
467+
return localize('mcp.newTools', "New tools available ({0})", servers.length);
468+
} else if (state === DisplayedState.Error) {
469+
return localize('mcp.toolError', "Error loading {0} tool(s)", servers.length);
470+
} else if (state === DisplayedState.Refreshing) {
471+
return localize('mcp.toolRefresh', "Discovering tools...");
472+
} else {
473+
return null;
474+
}
374475
}
375476

376477
protected override updateLabel(): void {

src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery {
119119
continue;
120120
}
121121

122+
122123
if (!nextDefinitions.length) {
123124
src.disposable.clear();
125+
src.serverDefinitions.set(nextDefinitions, undefined);
124126
} else {
125127
src.serverDefinitions.set(nextDefinitions, undefined);
126128
src.disposable.value ??= this._mcpRegistry.registerCollection({

src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
import { RunOnceScheduler } from '../../../../../base/common/async.js';
77
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
88
import { Schemas } from '../../../../../base/common/network.js';
9-
import { observableValue } from '../../../../../base/common/observable.js';
9+
import { autorunWithStore, IObservable, observableValue } from '../../../../../base/common/observable.js';
1010
import { URI } from '../../../../../base/common/uri.js';
1111
import { localize } from '../../../../../nls.js';
12+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
1213
import { IFileService } from '../../../../../platform/files/common/files.js';
1314
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1415
import { ILabelService } from '../../../../../platform/label/common/label.js';
1516
import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js';
17+
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
1618
import { StorageScope } from '../../../../../platform/storage/common/storage.js';
1719
import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
20+
import { mcpDiscoverySection } from '../mcpConfiguration.js';
1821
import { IMcpRegistry } from '../mcpRegistryTypes.js';
1922
import { McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition } from '../mcpTypes.js';
2023
import { IMcpDiscovery } from './mcpDiscovery.js';
@@ -26,6 +29,7 @@ import { ClaudeDesktopMpcDiscoveryAdapter, NativeMpcDiscoveryAdapter } from './n
2629
*/
2730
export abstract class FilesystemMpcDiscovery extends Disposable implements IMcpDiscovery {
2831
private readonly adapters: readonly NativeMpcDiscoveryAdapter[];
32+
private _fsDiscoveryEnabled: IObservable<boolean>;
2933
private suffix = '';
3034

3135
constructor(
@@ -34,12 +38,15 @@ export abstract class FilesystemMpcDiscovery extends Disposable implements IMcpD
3438
@IFileService private readonly fileService: IFileService,
3539
@IInstantiationService instantiationService: IInstantiationService,
3640
@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,
41+
@IConfigurationService configurationService: IConfigurationService,
3742
) {
3843
super();
3944
if (remoteAuthority) {
4045
this.suffix = ' ' + localize('onRemoteLabel', ' on {0}', labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority));
4146
}
4247

48+
this._fsDiscoveryEnabled = observableConfigValue(mcpDiscoverySection, false, configurationService);
49+
4350
this.adapters = [
4451
instantiationService.createInstance(ClaudeDesktopMpcDiscoveryAdapter, remoteAuthority)
4552
];
@@ -97,10 +104,17 @@ export abstract class FilesystemMpcDiscovery extends Disposable implements IMcpD
97104
}
98105
};
99106

100-
const watcher = this._register(this.fileService.createWatcher(file, { recursive: false, excludes: [] }));
101-
const throttler = this._register(new RunOnceScheduler(updateFile, 500));
102-
this._register(watcher.onDidChange(() => throttler.schedule()));
103-
updateFile();
107+
this._register(autorunWithStore((reader, store) => {
108+
if (!this._fsDiscoveryEnabled.read(reader)) {
109+
collectionRegistration.clear();
110+
return;
111+
}
112+
113+
const throttler = store.add(new RunOnceScheduler(updateFile, 500));
114+
const watcher = store.add(this.fileService.createWatcher(file, { recursive: false, excludes: [] }));
115+
store.add(watcher.onDidChange(() => throttler.schedule()));
116+
updateFile();
117+
}));
104118
}
105119
}
106120
}

src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { ProxyChannel } from '../../../../../base/parts/ipc/common/ipc.js';
7+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
78
import { IFileService } from '../../../../../platform/files/common/files.js';
89
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
910
import { ILabelService } from '../../../../../platform/label/common/label.js';
@@ -24,8 +25,9 @@ export class RemoteNativeMpcDiscovery extends FilesystemMpcDiscovery {
2425
@IFileService fileService: IFileService,
2526
@IInstantiationService instantiationService: IInstantiationService,
2627
@IMcpRegistry mcpRegistry: IMcpRegistry,
28+
@IConfigurationService configurationService: IConfigurationService,
2729
) {
28-
super(remoteAgent.getConnection()?.remoteAuthority || null, labelService, fileService, instantiationService, mcpRegistry);
30+
super(remoteAgent.getConnection()?.remoteAuthority || null, labelService, fileService, instantiationService, mcpRegistry, configurationService);
2931
}
3032

3133
public override async start() {

src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const mcpSchemaExampleServer = {
1717
};
1818

1919
export const mcpConfigurationSection = 'mcp';
20+
export const mcpDiscoverySection = 'chat.mpc.discovery.enabled';
2021

2122
export const mcpSchemaExampleServers = {
2223
'mcp-server-time': {

0 commit comments

Comments
 (0)