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

297 LOC · 184 covered · 113 uncovered · 26 ranges · 88 concepts · 8 introducers · 53 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mcpService.ts ×13
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 { RunOnceScheduler } from '../../../../base/common/async.js';
7 > import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8 > import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
9 > import { autorun, derived, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';
10 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
11 > import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
12 > import { ILogService } from '../../../../platform/log/common/log.js';
13 > import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
14 > import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
15 > import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js';
16 > import { CollisionEnablementModel, EnablementModel, isContributionEnabled } from '../../chat/common/enablement.js';
17 > import { McpCollisionBehavior, mcpServerCollisionBehaviorSection } from './mcpConfiguration.js';
18 > import { IMcpRegistry } from './mcpRegistryTypes.js';
19 > import { McpPrefixGenerator, McpServer, McpServerMetadataCache } from './mcpServer.js';
20 > import { IAutostartResult, IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, UserInteractionRequiredError } from './mcpTypes.js';
21 > import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';
22 >
23 > type IMcpServerRec = { object: IMcpServer };
24 >
25 > export class McpService extends Disposable implements IMcpService {
26 >
27 > declare _serviceBrand: undefined;
28 >
29 > private readonly _currentAutoStarts = new Set<CancellationTokenSource>();
30 > private readonly _servers = observableValue<readonly IMcpServerRec[]>(this, []);
31 > public readonly servers: IObservable<readonly IMcpServer[]> = this._servers.map(servers => servers.map(s => s.object));
32 >
33 > private readonly _prefixGenerator = new McpPrefixGenerator();
34 >
35 > public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; }
36 >
37 > public readonly enablementModel: McpCollisionEnablementModel;
38 >
39 > protected readonly userCache: McpServerMetadataCache;
40 > protected readonly workspaceCache: McpServerMetadataCache;
41 >
42 > constructor(
43 > @IInstantiationService private readonly _instantiationService: IInstantiationService, mcpServer.ts ×23
44 > @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
45 > @ILogService private readonly _logService: ILogService,
46 > @IConfigurationService private readonly configurationService: IConfigurationService,
47 > @IStorageService storageService: IStorageService,
48 > ) {
49 > super();
50 >
51 > const baseEnablement = this._register(new EnablementModel('mcp.enablement', storageService));
52 > const collisionBehavior = observableConfigValue(mcpServerCollisionBehaviorSection, McpCollisionBehavior.Disable, configurationService);
53 > this.enablementModel = new McpCollisionEnablementModel(baseEnablement, this._mcpRegistry, collisionBehavior);
54 >
55 > this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE));
56 > this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE));
57 >
58 > const updateThrottle = this._store.add(new RunOnceScheduler(() => this.updateCollectedServers(), 500));
59 >
60 > // Throttle changes so that if a collection is changed, or a server is
61 > // unregistered/registered, we don't stop servers unnecessarily.
62 > this._register(autorun(reader => {
63 > for (const collection of this._mcpRegistry.collections.read(reader)) {
64 > collection.serverDefinitions.read(reader);
65 > }
66 > updateThrottle.schedule(500);
67 > }));
68 > }
70 > public cancelAutostart(): void {
71 for (const cts of this._currentAutoStarts) {
72 cts.cancel();
73 }
74 }
76 > public autostart(_token?: CancellationToken): IObservable<IAutostartResult> {
77 const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);
78 if (autoStartConfig === McpAutoStartValue.Never) {
79 return observableValue<IAutostartResult>(this, IAutostartResult.Empty);
80 }
81
82 const state = observableValue<IAutostartResult>(this, { working: true, starting: [], serversRequiringInteraction: [] });
83 const store = new DisposableStore();
84
85 const cts = store.add(new CancellationTokenSource(_token));
86 this._currentAutoStarts.add(cts);
87 store.add(toDisposable(() => {
88 this._currentAutoStarts.delete(cts);
89 }));
90 store.add(cts.token.onCancellationRequested(() => {
91 state.set(IAutostartResult.Empty, undefined);
92 }));
93
94 this._autostart(autoStartConfig, state, cts.token)
95 .catch(err => {
96 this._logService.error('Error during MCP autostart:', err);
97 state.set(IAutostartResult.Empty, undefined);
98 })
99 .finally(() => store.dispose());
100
101 return state;
102 }
104 > private async _autostart(autoStartConfig: McpAutoStartValue, state: ISettableObservable<IAutostartResult>, token: CancellationToken) {
105 await this._activateCollections();
106
107 if (token.isCancellationRequested) {
108 return;
109 }
110
111 // don't try re-running errored servers or disabled servers
112 const candidates = this.servers.get().filter(s =>
113 s.connectionState.get().state !== McpConnectionState.Kind.Error
114 && isContributionEnabled(s.enablement.get())
115 );
116
117 let todo = new Set<IMcpServer>();
118 if (autoStartConfig === McpAutoStartValue.OnlyNew) {
119 todo = new Set(candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown));
120 } else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {
121 todo = new Set(candidates.filter(s => {
122 const c = s.cacheState.get();
123 return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;
124 }));
125 }
126
127 if (!todo.size) {
128 state.set(IAutostartResult.Empty, undefined);
129 return;
130 }
131
132 const interaction = new McpStartServerInteraction();
133 const requiringInteraction: (McpDefinitionReference & { errorMessage?: string })[] = [];
134
135 const update = () => state.set({
136 working: todo.size > 0,
137 starting: [...todo].map(t => t.definition),
138 serversRequiringInteraction: requiringInteraction,
139 }, undefined);
140
141 update();
142
143 await Promise.all([...todo].map(async (server, i) => {
144 try {
145 await startServerAndWaitForLiveTools(server, { interaction, errorOnUserInteraction: true }, token);
146 } catch (error) {
147 if (error instanceof UserInteractionRequiredError) {
148 requiringInteraction.push({ id: server.definition.id, label: server.definition.label, errorMessage: error.message });
149 }
150 } finally {
151 todo.delete(server);
152 if (!token.isCancellationRequested) {
153 update();
154 }
155 }
156 }));
157 }
159 > public resetCaches(): void {
160 this.userCache.reset();
161 this.workspaceCache.reset();
162 }
164 > public resetTrust(): void {
165 this.resetCaches(); // same difference now
166 }
168 > public async activateCollections(): Promise<void> {
169 await this._activateCollections();
170 }
172 > private async _activateCollections() {
173 const collections = await this._mcpRegistry.discoverCollections();
174 this.updateCollectedServers();
175 return new Set(collections.map(c => c.id));
176 }
178 > public updateCollectedServers() {
179 > const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition => mcpServer.ts ×23
180 > collectionDefinition.serverDefinitions.get().map(serverDefinition => {
181 > return { serverDefinition, collectionDefinition };
182 > })
183 > );
184 >
185 > const nextDefinitions = new Set(definitions);
186 > const currentServers = this._servers.get();
187 > const nextServers: IMcpServerRec[] = [];
188 > const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => {
189 nextDefinitions.delete(match);
190 nextServers.push(rec);
191 const connection = rec.object.connection.get();
192 // if the definition was modified, stop the server; it'll be restarted again on-demand
193 if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) {
194 rec.object.stop();
195 this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`);
196 }
197 };
199 > // Transfer over any servers that are still valid.
200 > for (const server of currentServers) {
201 const match = definitions.find(d => defsEqual(server.object, d));
202 if (match) {
203 pushMatch(match, server);
204 } else {
205 server.object.dispose();
206 }
207 }
209 > // Create any new servers that are needed.
210 > for (const def of nextDefinitions) {
211 > const object = this._instantiationService.createInstance(
212 > McpServer,
213 > def.collectionDefinition,
214 > def.serverDefinition,
215 > def.serverDefinition.roots,
216 > !!def.collectionDefinition.lazy,
217 > def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,
218 > this._prefixGenerator,
219 > this.enablementModel,
220 > );
221 >
222 > nextServers.push({ object });
223 > }
224 >
225 > transaction(tx => {
226 > this._servers.set(nextServers, tx);
227 > });
228 > }
230 > public override dispose(): void {
231 > this._servers.get().forEach(s => s.object.dispose()); mcpServer.ts ×23
232 > super.dispose();
233 > }
235 >
236 function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) {
237 return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id;
238 }
240 > /**
241 > * Wraps an {@link EnablementModel} with collision-aware defaults and
242 > * mutual-exclusion logic for MCP servers with the same label.
243 > *
244 > * When collision behavior is `disable`:
245 > * - Servers whose label collides with a higher-priority server are disabled
246 > * by default (unless the user has explicitly toggled them).
247 > * - Enabling a colliding server disables all other servers with the same label.
248 > *
249 > * When collision behavior is `suffix`, delegates everything unchanged.
250 > */
251 > export class McpCollisionEnablementModel extends CollisionEnablementModel {
252 >
253 > /**
254 > * For each server definition ID, the list of all definition IDs that share
255 > * the same (case-insensitive) label, in priority order (lowest collection
256 > * order first). Empty when collision behavior is `suffix`.
257 > */
258 > constructor(
259 > base: EnablementModel, mcpService.ts ×2
260 > registry: IMcpRegistry,
261 > collisionBehavior: IObservable<McpCollisionBehavior>,
262 > ) {
263 > const collisionGroups = derived(reader => {
264 > if (collisionBehavior.read(reader) !== McpCollisionBehavior.Disable) { enablement.ts ×3
265 > return new Map<string, string[]>(); mcpService.ts ×1
266 > }
268 > const collections = registry.collections.read(reader);
269 > // label → list of server definition IDs, in priority order
270 > const labelToIds = new Map<string, string[]>();
271 > for (const collection of collections) {
272 > for (const server of collection.serverDefinitions.read(reader)) {
273 > const key = server.label.toLowerCase();
274 > let ids = labelToIds.get(key);
275 > if (!ids) {
276 > ids = [];
277 > labelToIds.set(key, ids);
278 > }
279 > ids.push(server.id);
280 > }
281 > }
282 >
283 > const groups = new Map<string, string[]>();
284 > for (const ids of labelToIds.values()) {
285 > if (ids.length < 2) {
286 > continue; mcpService.ts ×1
287 > }
288 > for (const id of ids) { mcpService.ts ×1
289 > groups.set(id, ids);
290 > }
291 > }
293 > return groups;
294 > }); mcpService.ts ×2
295 > super(base, collisionGroups);
296 > }