Skip to content

Commit 7b36e11

Browse files
authored
1 parent 94ea5b3 commit 7b36e11

File tree

3 files changed

+135
-25
lines changed

3 files changed

+135
-25
lines changed

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

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import './media/mcpServersView.css';
77
import * as dom from '../../../../base/browser/dom.js';
88
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
99
import { IListContextMenuEvent, IListRenderer } from '../../../../base/browser/ui/list/list.js';
10-
import { Event } from '../../../../base/common/event.js';
10+
import { Emitter, Event } from '../../../../base/common/event.js';
1111
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
1212
import { DelayedPagedModel, IPagedModel, PagedModel } from '../../../../base/common/paging.js';
1313
import { localize, localize2 } from '../../../../nls.js';
@@ -24,7 +24,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js
2424
import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/views/viewPane.js';
2525
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
2626
import { IViewDescriptorService, IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js';
27-
import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js';
27+
import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon, McpServerInstallState } from '../common/mcpTypes.js';
2828
import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js';
2929
import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js';
3030
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
@@ -47,8 +47,10 @@ export interface McpServerListViewOptions {
4747
}
4848

4949
interface IQueryResult {
50-
showWelcomeContent: boolean;
5150
model: IPagedModel<IWorkbenchMcpServer>;
51+
disposables: DisposableStore;
52+
showWelcomeContent?: boolean;
53+
onDidChangeModel?: Event<IPagedModel<IWorkbenchMcpServer>>;
5254
}
5355

5456
export class McpServersListView extends ViewPane {
@@ -157,18 +159,26 @@ export class McpServersListView extends ViewPane {
157159

158160
async show(query: string): Promise<IPagedModel<IWorkbenchMcpServer>> {
159161
if (this.input) {
162+
this.input.disposables.dispose();
160163
this.input = undefined;
161164
}
162165

163-
query = query.trim();
164-
const servers = query ? await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') }) : await this.mcpWorkbenchService.queryLocal();
165-
const showWelcomeContent = !this.mcpGalleryService.isEnabled() && servers.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty;
166-
167-
const model = new PagedModel(servers);
168-
this.input = { model, showWelcomeContent };
166+
this.input = await this.query(query.trim());
167+
this.input.showWelcomeContent = !this.mcpGalleryService.isEnabled() && this.input.model.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty;
169168
this.renderInput();
170169

171-
return model;
170+
if (this.input.onDidChangeModel) {
171+
this.input.disposables.add(this.input.onDidChangeModel(model => {
172+
if (!this.input) {
173+
return;
174+
}
175+
this.input.model = model;
176+
this.input.showWelcomeContent = !this.mcpGalleryService.isEnabled() && this.input.model.length === 0 && !!this.mpcViewOptions.showWelcomeOnEmpty;
177+
this.renderInput();
178+
}));
179+
}
180+
181+
return this.input.model;
172182
}
173183

174184
private renderInput() {
@@ -178,7 +188,7 @@ export class McpServersListView extends ViewPane {
178188
if (this.list) {
179189
this.list.model = new DelayedPagedModel(this.input.model);
180190
}
181-
this.showWelcomeContent(this.input.showWelcomeContent);
191+
this.showWelcomeContent(!!this.input.showWelcomeContent);
182192
}
183193

184194
private showWelcomeContent(show: boolean): void {
@@ -212,6 +222,51 @@ export class McpServersListView extends ViewPane {
212222
description.appendChild(markdownResult.element);
213223
}
214224

225+
private async query(query: string): Promise<IQueryResult> {
226+
const disposables = new DisposableStore();
227+
if (query) {
228+
const servers = await this.mcpWorkbenchService.queryGallery({ text: query.replace('@mcp', '') });
229+
return { model: new PagedModel(servers), disposables };
230+
}
231+
232+
const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IWorkbenchMcpServer>>());
233+
let servers = await this.mcpWorkbenchService.queryLocal();
234+
disposables.add(Event.debounce(Event.filter(this.mcpWorkbenchService.onChange, e => e?.installState === McpServerInstallState.Installed), () => undefined)(async () => {
235+
const mergedMcpServers = this.mergeAddedMcpServers(servers, [...this.mcpWorkbenchService.local]);
236+
if (mergedMcpServers) {
237+
servers = mergedMcpServers;
238+
onDidChangeModel.fire(new PagedModel(servers));
239+
}
240+
}));
241+
return { model: new PagedModel(servers), onDidChangeModel: onDidChangeModel.event, disposables };
242+
}
243+
244+
private mergeAddedMcpServers(mcpServers: IWorkbenchMcpServer[], newMcpServers: IWorkbenchMcpServer[]): IWorkbenchMcpServer[] | undefined {
245+
const oldMcpServers = [...mcpServers];
246+
const findPreviousMcpServerIndex = (from: number): number => {
247+
let index = -1;
248+
const previousMcpServerInNew = newMcpServers[from];
249+
if (previousMcpServerInNew) {
250+
index = oldMcpServers.findIndex(e => e.name === previousMcpServerInNew.name);
251+
if (index === -1) {
252+
return findPreviousMcpServerIndex(from - 1);
253+
}
254+
}
255+
return index;
256+
};
257+
258+
let hasChanged: boolean = false;
259+
for (let index = 0; index < newMcpServers.length; index++) {
260+
const mcpServer = newMcpServers[index];
261+
if (mcpServers.every(r => r.name !== mcpServer.name)) {
262+
hasChanged = true;
263+
mcpServers.splice(findPreviousMcpServerIndex(index - 1) + 1, 0, mcpServer);
264+
}
265+
}
266+
267+
return hasChanged ? mcpServers : undefined;
268+
}
269+
215270
}
216271

217272
interface IMcpServerTemplateData {

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

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationToken } from '../../../../base/common/cancellation.js';
7-
import { Emitter } from '../../../../base/common/event.js';
7+
import { Emitter, Event } from '../../../../base/common/event.js';
88
import { Disposable } from '../../../../base/common/lifecycle.js';
99
import { Schemas } from '../../../../base/common/network.js';
1010
import { basename } from '../../../../base/common/resources.js';
@@ -32,12 +32,17 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm
3232
import { IWorkbenchLocalMcpServer, IWorkbenchMcpManagementService, LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
3333
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
3434
import { mcpConfigurationSection } from '../common/mcpConfiguration.js';
35-
import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
35+
import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerInstallState, McpServersGalleryEnabledContext } from '../common/mcpTypes.js';
3636
import { McpServerEditorInput } from './mcpServerEditorInput.js';
3737

38+
interface IMcpServerStateProvider<T> {
39+
(mcpWorkbenchServer: McpWorkbenchServer): T;
40+
}
41+
3842
class McpWorkbenchServer implements IWorkbenchMcpServer {
3943

4044
constructor(
45+
private installStateProvider: IMcpServerStateProvider<McpServerInstallState>,
4146
public local: IWorkbenchLocalMcpServer | undefined,
4247
public gallery: IGalleryMcpServer | undefined,
4348
public readonly installable: IInstallableMcpServer | undefined,
@@ -66,6 +71,10 @@ class McpWorkbenchServer implements IWorkbenchMcpServer {
6671
return this.gallery?.icon ?? this.local?.icon;
6772
}
6873

74+
get installState(): McpServerInstallState {
75+
return this.installStateProvider(this);
76+
}
77+
6978
get codicon(): string | undefined {
7079
return this.gallery?.codicon ?? this.local?.codicon;
7180
}
@@ -133,8 +142,11 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
133142

134143
_serviceBrand: undefined;
135144

145+
private installing: McpWorkbenchServer[] = [];
146+
private uninstalling: McpWorkbenchServer[] = [];
147+
136148
private _local: McpWorkbenchServer[] = [];
137-
get local(): readonly McpWorkbenchServer[] { return this._local; }
149+
get local(): readonly McpWorkbenchServer[] { return [...this._local]; }
138150

139151
private readonly _onChange = this._register(new Emitter<IWorkbenchMcpServer | undefined>());
140152
readonly onChange = this._onChange.event;
@@ -191,7 +203,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
191203
if (server) {
192204
server.local = local;
193205
} else {
194-
server = this.instantiationService.createInstance(McpWorkbenchServer, local, gallery, undefined);
206+
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, gallery, undefined);
195207
this._local.push(server);
196208
}
197209
this._onChange.fire(server);
@@ -209,7 +221,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
209221
this._local[serverIndex].local = result.local;
210222
server = this._local[serverIndex];
211223
} else {
212-
server = this.instantiationService.createInstance(McpWorkbenchServer, result.local, result.source, undefined);
224+
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), result.local, result.source, undefined);
213225
this._local.push(server);
214226
}
215227
this._onChange.fire(server);
@@ -270,28 +282,32 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
270282
return [];
271283
}
272284
const result = await this.mcpGalleryService.query(options, token);
273-
return result.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, gallery, undefined));
285+
return result.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined));
274286
}
275287

276288
async queryLocal(): Promise<IWorkbenchMcpServer[]> {
277289
const installed = await this.mcpManagementService.getInstalled();
278290
this._local = installed.map(i => {
279-
const local = this._local.find(server => server.name === i.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, undefined, undefined, undefined);
291+
const local = this._local.find(server => server.name === i.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined);
280292
local.local = i;
281293
return local;
282294
});
283-
return this._local;
295+
return [...this.local];
284296
}
285297

286298
async install(server: IWorkbenchMcpServer): Promise<IWorkbenchMcpServer> {
299+
if (!(server instanceof McpWorkbenchServer)) {
300+
throw new Error('Invalid server instance');
301+
}
302+
287303
if (server.installable) {
288-
const local = await this.mcpManagementService.install(server.installable);
289-
return this.onDidInstallMcpServer(local);
304+
const installable = server.installable;
305+
return this.doInstall(server, () => this.mcpManagementService.install(installable));
290306
}
291307

292308
if (server.gallery) {
293-
const local = await this.mcpManagementService.installFromGallery(server.gallery, { packageType: server.gallery.packageTypes[0] });
294-
return this.onDidInstallMcpServer(local);
309+
const gallery = server.gallery;
310+
return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery, { packageType: gallery.packageTypes[0] }));
295311
}
296312

297313
throw new Error('No installable server found');
@@ -304,6 +320,26 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
304320
await this.mcpManagementService.uninstall(server.local);
305321
}
306322

323+
private async doInstall(server: McpWorkbenchServer, installTask: () => Promise<IWorkbenchLocalMcpServer>): Promise<IWorkbenchMcpServer> {
324+
this.installing.push(server);
325+
this._onChange.fire(server);
326+
await installTask().finally(() => this.installing = this.installing.filter(s => s !== server));
327+
return this.waitAndGetInstalledMcpServer(server);
328+
}
329+
330+
private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise<IWorkbenchMcpServer> {
331+
let installed = this.local.find(local => local.name === server.name);
332+
if (!installed) {
333+
await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name)));
334+
}
335+
installed = this.local.find(local => local.name === server.name);
336+
if (!installed) {
337+
// This should not happen
338+
throw new Error('Extension should have been installed');
339+
}
340+
return installed;
341+
}
342+
307343
getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;
308344
getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;
309345
getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {
@@ -422,12 +458,12 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
422458
if (!galleryServer) {
423459
throw new Error(`MCP server '${name}' not found in gallery`);
424460
}
425-
this.open(this.instantiationService.createInstance(McpWorkbenchServer, undefined, galleryServer, undefined));
461+
this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, galleryServer, undefined));
426462
} else {
427463
if (config.type === undefined) {
428464
(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE;
429465
}
430-
this.open(this.instantiationService.createInstance(McpWorkbenchServer, undefined, undefined, { name, config, inputs }));
466+
this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, { name, config, inputs }));
431467
}
432468
} catch (e) {
433469
// ignore
@@ -439,6 +475,17 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ
439475
await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP);
440476
}
441477

478+
private getInstallState(extension: McpWorkbenchServer): McpServerInstallState {
479+
if (this.installing.some(i => i.name === extension.name)) {
480+
return McpServerInstallState.Installing;
481+
}
482+
if (this.uninstalling.some(e => e.name === extension.name)) {
483+
return McpServerInstallState.Uninstalling;
484+
}
485+
const local = this.local.find(e => e === extension);
486+
return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled;
487+
}
488+
442489
}
443490

444491
export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,10 +569,18 @@ export interface IMcpServerContainer extends IDisposable {
569569
update(): void;
570570
}
571571

572+
export const enum McpServerInstallState {
573+
Installing,
574+
Installed,
575+
Uninstalling,
576+
Uninstalled
577+
}
578+
572579
export interface IWorkbenchMcpServer {
573580
readonly gallery: IGalleryMcpServer | undefined;
574581
readonly local: IWorkbenchLocalMcpServer | undefined;
575582
readonly installable: IInstallableMcpServer | undefined;
583+
readonly installState: McpServerInstallState;
576584
readonly id: string;
577585
readonly name: string;
578586
readonly label: string;

0 commit comments

Comments
 (0)