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
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 { RunOnceScheduler } from '../../../../base/common/async.js';
7
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';
10
import { localize } from '../../../../nls.js';
11
import { ICommandService } from '../../../../platform/commands/common/commands.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
14
import { ILogService } from '../../../../platform/log/common/log.js';
15
import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
16
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
17
import { StorageScope } from '../../../../platform/storage/common/storage.js';
18
import { IMcpRegistry } from './mcpRegistryTypes.js';
19
import { McpServer, McpServerMetadataCache } from './mcpServer.js';
20
import { IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName } from './mcpTypes.js';
21
import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';
22
23
type IMcpServerRec = { object: IMcpServer; toolPrefix: string };
24
25
export class McpService extends Disposable implements IMcpService {
26
27
declare _serviceBrand: undefined;
28
29
private readonly _servers = observableValue<readonly IMcpServerRec[]>(this, []);
30
public readonly servers: IObservable<readonly IMcpServer[]> = this._servers.map(servers => servers.map(s => s.object));
31
32
public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; }
33
34
protected readonly userCache: McpServerMetadataCache;
35
protected readonly workspaceCache: McpServerMetadataCache;
36
37
constructor(
38
@IInstantiationService private readonly _instantiationService: IInstantiationService,
39
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
40
@ILogService private readonly _logService: ILogService,
41
@IProgressService private readonly progressService: IProgressService,
42
@ICommandService private readonly commandService: ICommandService,
43
@IConfigurationService private readonly configurationService: IConfigurationService
44
) {
45
super();
46
47
this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE));
48
this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE));
49
50
const updateThrottle = this._store.add(new RunOnceScheduler(() => this.updateCollectedServers(), 500));
51
52
// Throttle changes so that if a collection is changed, or a server is
53
// unregistered/registered, we don't stop servers unnecessarily.
54
this._register(autorun(reader => {
55
for (const collection of this._mcpRegistry.collections.read(reader)) {
56
collection.serverDefinitions.read(reader);
57
}
58
updateThrottle.schedule(500);
59
}));
60
}
61
62
public async autostart(token?: CancellationToken): Promise<void> {
63
const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);
64
65
// don't try re-running errored servers, let the user choose if they want that
66
const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);
67
68
let todo: IMcpServer[] = [];
69
if (autoStartConfig === McpAutoStartValue.OnlyNew) {
70
todo = candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown);
71
} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {
72
todo = candidates.filter(s => {
73
const c = s.cacheState.get();
74
return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;
75
});
76
}
77
78
if (!todo.length) {
79
return;
80
}
81
82
const interaction = new McpStartServerInteraction();
83
const cts = new CancellationTokenSource(token);
84
85
await this.progressService.withProgress(
86
{
87
location: ProgressLocation.Notification,
88
cancellable: true,
89
delay: 5_000,
90
total: todo.length,
91
buttons: [
92
localize('mcp.autostart.send', 'Skip Waiting'),
93
localize('mcp.autostart.configure', 'Configure'),
94
]
95
},
96
report => {
97
const remaining = new Set(todo);
98
const doReport = () => report.report({ message: localize('mcp.autostart.progress', 'Waiting for MCP server "{0}" to start...', [...remaining].map(r => r.definition.label).join('", "')), total: todo.length, increment: 1 });
99
doReport();
100
return Promise.all(todo.map(async server => {
101
await startServerAndWaitForLiveTools(server, { interaction }, cts.token);
102
remaining.delete(server);
103
doReport();
104
}));
105
},
106
btn => {
107
if (btn === 1) {
108
this.commandService.executeCommand('workbench.action.openSettings', mcpAutoStartConfig);
109
}
110
cts.cancel();
111
},
112
);
113
114
cts.dispose();
115
}
116
117
public resetCaches(): void {
118
this.userCache.reset();
119
this.workspaceCache.reset();
120
}
121
122
public resetTrust(): void {
123
this.resetCaches(); // same difference now
124
}
125
126
public async activateCollections(): Promise<void> {
127
const collections = await this._mcpRegistry.discoverCollections();
128
const collectionIds = new Set(collections.map(c => c.id));
129
130
this.updateCollectedServers();
131
132
// Discover any newly-collected servers with unknown tools
133
const todo: Promise<unknown>[] = [];
134
for (const { object: server } of this._servers.get()) {
135
if (collectionIds.has(server.collection.id)) {
136
const state = server.cacheState.get();
137
if (state === McpServerCacheState.Unknown) {
138
todo.push(server.start());
139
}
140
}
141
}
142
143
await Promise.all(todo);
144
}
145
146
public updateCollectedServers() {
147
const prefixGenerator = new McpPrefixGenerator();
148
const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition =>
149
collectionDefinition.serverDefinitions.get().map(serverDefinition => {
150
const toolPrefix = prefixGenerator.generate(serverDefinition.label);
151
return { serverDefinition, collectionDefinition, toolPrefix };
152
})
153
);
154
155
const nextDefinitions = new Set(definitions);
156
const currentServers = this._servers.get();
157
const nextServers: IMcpServerRec[] = [];
158
const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => {
159
nextDefinitions.delete(match);
160
nextServers.push(rec);
161
const connection = rec.object.connection.get();
162
// if the definition was modified, stop the server; it'll be restarted again on-demand
163
if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) {
164
rec.object.stop();
165
this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`);
166
}
167
};
168
169
// Transfer over any servers that are still valid.
170
for (const server of currentServers) {
171
const match = definitions.find(d => defsEqual(server.object, d) && server.toolPrefix === d.toolPrefix);
172
if (match) {
173
pushMatch(match, server);
174
} else {
175
server.object.dispose();
176
}
177
}
178
179
// Create any new servers that are needed.
180
for (const def of nextDefinitions) {
181
const object = this._instantiationService.createInstance(
182
McpServer,
183
def.collectionDefinition,
184
def.serverDefinition,
185
def.serverDefinition.roots,
186
!!def.collectionDefinition.lazy,
187
def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,
188
def.toolPrefix,
189
);
190
191
nextServers.push({ object, toolPrefix: def.toolPrefix });
192
}
193
194
transaction(tx => {
195
this._servers.set(nextServers, tx);
196
});
197
}
198
199
public override dispose(): void {
200
this._servers.get().forEach(s => s.object.dispose());
201
super.dispose();
202
}
203
}
204
205
function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) {
206
return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id;
207
}
208
209
// Helper class for generating unique MCP tool prefixes
210
class McpPrefixGenerator {
211
private readonly seenPrefixes = new Set<string>();
212
213
generate(label: string): string {
214
const baseToolPrefix = McpToolName.Prefix + label.toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, McpToolName.MaxPrefixLen - McpToolName.Prefix.length - 1);
215
let toolPrefix = baseToolPrefix + '_';
216
for (let i = 2; this.seenPrefixes.has(toolPrefix); i++) {
217
toolPrefix = baseToolPrefix + i + '_';
218
}
219
this.seenPrefixes.add(toolPrefix);
220
return toolPrefix;
221
}
222
}
223
224