Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpService.ts
5263 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 { 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, 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 { StorageScope } from '../../../../platform/storage/common/storage.js';
15
import { IMcpRegistry } from './mcpRegistryTypes.js';
16
import { McpServer, McpServerMetadataCache } from './mcpServer.js';
17
import { IAutostartResult, IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName, UserInteractionRequiredError } from './mcpTypes.js';
18
import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';
19
20
type IMcpServerRec = { object: IMcpServer; toolPrefix: string };
21
22
export class McpService extends Disposable implements IMcpService {
23
24
declare _serviceBrand: undefined;
25
26
private readonly _currentAutoStarts = new Set<CancellationTokenSource>();
27
private readonly _servers = observableValue<readonly IMcpServerRec[]>(this, []);
28
public readonly servers: IObservable<readonly IMcpServer[]> = this._servers.map(servers => servers.map(s => s.object));
29
30
public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; }
31
32
protected readonly userCache: McpServerMetadataCache;
33
protected readonly workspaceCache: McpServerMetadataCache;
34
35
constructor(
36
@IInstantiationService private readonly _instantiationService: IInstantiationService,
37
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
38
@ILogService private readonly _logService: ILogService,
39
@IConfigurationService private readonly configurationService: IConfigurationService
40
) {
41
super();
42
43
this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE));
44
this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE));
45
46
const updateThrottle = this._store.add(new RunOnceScheduler(() => this.updateCollectedServers(), 500));
47
48
// Throttle changes so that if a collection is changed, or a server is
49
// unregistered/registered, we don't stop servers unnecessarily.
50
this._register(autorun(reader => {
51
for (const collection of this._mcpRegistry.collections.read(reader)) {
52
collection.serverDefinitions.read(reader);
53
}
54
updateThrottle.schedule(500);
55
}));
56
}
57
58
public cancelAutostart(): void {
59
for (const cts of this._currentAutoStarts) {
60
cts.cancel();
61
}
62
}
63
64
public autostart(_token?: CancellationToken): IObservable<IAutostartResult> {
65
const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);
66
if (autoStartConfig === McpAutoStartValue.Never) {
67
return observableValue<IAutostartResult>(this, IAutostartResult.Empty);
68
}
69
70
const state = observableValue<IAutostartResult>(this, { working: true, starting: [], serversRequiringInteraction: [] });
71
const store = new DisposableStore();
72
73
const cts = store.add(new CancellationTokenSource(_token));
74
this._currentAutoStarts.add(cts);
75
store.add(toDisposable(() => {
76
this._currentAutoStarts.delete(cts);
77
}));
78
store.add(cts.token.onCancellationRequested(() => {
79
state.set(IAutostartResult.Empty, undefined);
80
}));
81
82
this._autostart(autoStartConfig, state, cts.token)
83
.catch(err => {
84
this._logService.error('Error during MCP autostart:', err);
85
state.set(IAutostartResult.Empty, undefined);
86
})
87
.finally(() => store.dispose());
88
89
return state;
90
}
91
92
private async _autostart(autoStartConfig: McpAutoStartValue, state: ISettableObservable<IAutostartResult>, token: CancellationToken) {
93
await this._activateCollections();
94
95
if (token.isCancellationRequested) {
96
return;
97
}
98
99
// don't try re-running errored servers, let the user choose if they want that
100
const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);
101
102
let todo = new Set<IMcpServer>();
103
if (autoStartConfig === McpAutoStartValue.OnlyNew) {
104
todo = new Set(candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown));
105
} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {
106
todo = new Set(candidates.filter(s => {
107
const c = s.cacheState.get();
108
return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;
109
}));
110
}
111
112
if (!todo.size) {
113
state.set(IAutostartResult.Empty, undefined);
114
return;
115
}
116
117
const interaction = new McpStartServerInteraction();
118
const requiringInteraction: (McpDefinitionReference & { errorMessage?: string })[] = [];
119
120
const update = () => state.set({
121
working: todo.size > 0,
122
starting: [...todo].map(t => t.definition),
123
serversRequiringInteraction: requiringInteraction,
124
}, undefined);
125
126
update();
127
128
await Promise.all([...todo].map(async (server, i) => {
129
try {
130
await startServerAndWaitForLiveTools(server, { interaction, errorOnUserInteraction: true }, token);
131
} catch (error) {
132
if (error instanceof UserInteractionRequiredError) {
133
requiringInteraction.push({ id: server.definition.id, label: server.definition.label, errorMessage: error.message });
134
}
135
} finally {
136
todo.delete(server);
137
if (!token.isCancellationRequested) {
138
update();
139
}
140
}
141
}));
142
}
143
144
public resetCaches(): void {
145
this.userCache.reset();
146
this.workspaceCache.reset();
147
}
148
149
public resetTrust(): void {
150
this.resetCaches(); // same difference now
151
}
152
153
public async activateCollections(): Promise<void> {
154
await this._activateCollections();
155
}
156
157
private async _activateCollections() {
158
const collections = await this._mcpRegistry.discoverCollections();
159
this.updateCollectedServers();
160
return new Set(collections.map(c => c.id));
161
}
162
163
public updateCollectedServers() {
164
const prefixGenerator = new McpPrefixGenerator();
165
const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition =>
166
collectionDefinition.serverDefinitions.get().map(serverDefinition => {
167
const toolPrefix = prefixGenerator.generate(serverDefinition.label);
168
return { serverDefinition, collectionDefinition, toolPrefix };
169
})
170
);
171
172
const nextDefinitions = new Set(definitions);
173
const currentServers = this._servers.get();
174
const nextServers: IMcpServerRec[] = [];
175
const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => {
176
nextDefinitions.delete(match);
177
nextServers.push(rec);
178
const connection = rec.object.connection.get();
179
// if the definition was modified, stop the server; it'll be restarted again on-demand
180
if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) {
181
rec.object.stop();
182
this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`);
183
}
184
};
185
186
// Transfer over any servers that are still valid.
187
for (const server of currentServers) {
188
const match = definitions.find(d => defsEqual(server.object, d) && server.toolPrefix === d.toolPrefix);
189
if (match) {
190
pushMatch(match, server);
191
} else {
192
server.object.dispose();
193
}
194
}
195
196
// Create any new servers that are needed.
197
for (const def of nextDefinitions) {
198
const object = this._instantiationService.createInstance(
199
McpServer,
200
def.collectionDefinition,
201
def.serverDefinition,
202
def.serverDefinition.roots,
203
!!def.collectionDefinition.lazy,
204
def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,
205
def.toolPrefix,
206
);
207
208
nextServers.push({ object, toolPrefix: def.toolPrefix });
209
}
210
211
transaction(tx => {
212
this._servers.set(nextServers, tx);
213
});
214
}
215
216
public override dispose(): void {
217
this._servers.get().forEach(s => s.object.dispose());
218
super.dispose();
219
}
220
}
221
222
function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) {
223
return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id;
224
}
225
226
// Helper class for generating unique MCP tool prefixes
227
class McpPrefixGenerator {
228
private readonly seenPrefixes = new Set<string>();
229
230
generate(label: string): string {
231
const baseToolPrefix = McpToolName.Prefix + label.toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, McpToolName.MaxPrefixLen - McpToolName.Prefix.length - 1);
232
let toolPrefix = baseToolPrefix + '_';
233
for (let i = 2; this.seenPrefixes.has(toolPrefix); i++) {
234
toolPrefix = baseToolPrefix + i + '_';
235
}
236
this.seenPrefixes.add(toolPrefix);
237
return toolPrefix;
238
}
239
}
240
241