Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts
5363 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 { equals } from '../../../../../base/common/arrays.js';
7
import { Throttler } from '../../../../../base/common/async.js';
8
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
9
import { ResourceMap } from '../../../../../base/common/map.js';
10
import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { Location } from '../../../../../editor/common/languages.js';
13
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
14
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
15
import { ILogService } from '../../../../../platform/log/common/log.js';
16
import { StorageScope } from '../../../../../platform/storage/common/storage.js';
17
import { IWorkbenchLocalMcpServer } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js';
18
import { getMcpServerMapping } from '../mcpConfigFileUtils.js';
19
import { mcpConfigurationSection } from '../mcpConfiguration.js';
20
import { IMcpRegistry } from '../mcpRegistryTypes.js';
21
import { IMcpConfigPath, IMcpWorkbenchService, McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';
22
import { IMcpDiscovery } from './mcpDiscovery.js';
23
24
interface CollectionState extends IDisposable {
25
definition: McpCollectionDefinition;
26
serverDefinitions: ISettableObservable<readonly McpServerDefinition[]>;
27
}
28
29
export class InstalledMcpServersDiscovery extends Disposable implements IMcpDiscovery {
30
31
readonly fromGallery = true;
32
private readonly collections = this._register(new DisposableMap<string, CollectionState>());
33
34
constructor(
35
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
36
@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,
37
@ITextModelService private readonly textModelService: ITextModelService,
38
@ILogService private readonly logService: ILogService,
39
) {
40
super();
41
}
42
43
public start(): void {
44
const throttler = this._register(new Throttler());
45
this._register(this.mcpWorkbenchService.onChange(() => throttler.queue(() => this.sync())));
46
this.sync();
47
}
48
49
private async getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {
50
const store = new DisposableStore();
51
try {
52
const ref = await this.textModelService.createModelReference(resource);
53
store.add(ref);
54
const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });
55
return serverIdMapping;
56
} catch {
57
return new Map();
58
} finally {
59
store.dispose();
60
}
61
}
62
63
private async sync(): Promise<void> {
64
try {
65
const collections = new Map<string, [IMcpConfigPath | undefined, McpServerDefinition[]]>();
66
const mcpConfigPathInfos = new ResourceMap<Promise<IMcpConfigPath & { locations: Map<string, Location> } | undefined>>();
67
for (const server of this.mcpWorkbenchService.getEnabledLocalMcpServers()) {
68
let mcpConfigPathPromise = mcpConfigPathInfos.get(server.mcpResource);
69
if (!mcpConfigPathPromise) {
70
mcpConfigPathPromise = (async (local: IWorkbenchLocalMcpServer) => {
71
const mcpConfigPath = this.mcpWorkbenchService.getMcpConfigPath(local);
72
const locations = mcpConfigPath?.uri ? await this.getServerIdMapping(mcpConfigPath?.uri, mcpConfigPath.section ? [...mcpConfigPath.section, 'servers'] : ['servers']) : new Map();
73
return mcpConfigPath ? { ...mcpConfigPath, locations } : undefined;
74
})(server);
75
mcpConfigPathInfos.set(server.mcpResource, mcpConfigPathPromise);
76
}
77
78
const config = server.config;
79
const mcpConfigPath = await mcpConfigPathPromise;
80
const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;
81
82
let definitions = collections.get(collectionId);
83
if (!definitions) {
84
definitions = [mcpConfigPath, []];
85
collections.set(collectionId, definitions);
86
}
87
88
const launch: McpServerLaunch = config.type === 'http' ? {
89
type: McpServerTransportType.HTTP,
90
uri: URI.parse(config.url),
91
headers: Object.entries(config.headers || {}),
92
} : {
93
type: McpServerTransportType.Stdio,
94
command: config.command,
95
args: config.args || [],
96
env: config.env || {},
97
envFile: config.envFile,
98
cwd: config.cwd,
99
};
100
101
definitions[1].push({
102
id: `${collectionId}.${server.name}`,
103
label: server.name,
104
launch,
105
cacheNonce: await McpServerLaunch.hash(launch),
106
roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,
107
variableReplacement: {
108
folder: mcpConfigPath?.workspaceFolder,
109
section: mcpConfigurationSection,
110
target: mcpConfigPath?.target ?? ConfigurationTarget.USER,
111
},
112
devMode: config.dev,
113
presentation: {
114
order: mcpConfigPath?.order,
115
origin: mcpConfigPath?.locations.get(server.name)
116
}
117
});
118
}
119
120
for (const [id] of this.collections) {
121
if (!collections.has(id)) {
122
this.collections.deleteAndDispose(id);
123
}
124
}
125
126
for (const [id, [mcpConfigPath, serverDefinitions]] of collections) {
127
const newServerDefinitions = observableValue<readonly McpServerDefinition[]>(this, serverDefinitions);
128
const newCollection: McpCollectionDefinition = {
129
id,
130
label: mcpConfigPath?.label ?? '',
131
presentation: {
132
order: serverDefinitions[0]?.presentation?.order,
133
origin: mcpConfigPath?.uri,
134
},
135
remoteAuthority: mcpConfigPath?.remoteAuthority ?? null,
136
serverDefinitions: newServerDefinitions,
137
trustBehavior: McpServerTrust.Kind.Trusted,
138
configTarget: mcpConfigPath?.target ?? ConfigurationTarget.USER,
139
scope: mcpConfigPath?.scope ?? StorageScope.PROFILE,
140
};
141
const existingCollection = this.collections.get(id);
142
143
const collectionDefinitionsChanged = existingCollection ? !McpCollectionDefinition.equals(existingCollection.definition, newCollection) : true;
144
if (!collectionDefinitionsChanged) {
145
const serverDefinitionsChanged = existingCollection ? !equals(existingCollection.definition.serverDefinitions.get(), newCollection.serverDefinitions.get(), McpServerDefinition.equals) : true;
146
if (serverDefinitionsChanged) {
147
existingCollection?.serverDefinitions.set(serverDefinitions, undefined);
148
}
149
continue;
150
}
151
152
this.collections.deleteAndDispose(id);
153
const disposable = this.mcpRegistry.registerCollection(newCollection);
154
this.collections.set(id, {
155
definition: newCollection,
156
serverDefinitions: newServerDefinitions,
157
dispose: () => disposable.dispose()
158
});
159
}
160
161
} catch (error) {
162
this.logService.error(error);
163
}
164
}
165
}
166
167