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
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 { Throttler } from '../../../../../base/common/async.js';
7
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
8
import { ResourceMap } from '../../../../../base/common/map.js';
9
import { observableValue } from '../../../../../base/common/observable.js';
10
import { posix as pathPosix, sep as pathSep, win32 as pathWin32 } from '../../../../../base/common/path.js';
11
import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { Location } from '../../../../../editor/common/languages.js';
14
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
15
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
16
import { StorageScope } from '../../../../../platform/storage/common/storage.js';
17
import { IWorkbenchLocalMcpServer } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js';
18
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
19
import { getMcpServerMapping } from '../mcpConfigFileUtils.js';
20
import { mcpConfigurationSection } from '../mcpConfiguration.js';
21
import { IMcpRegistry } from '../mcpRegistryTypes.js';
22
import { IMcpConfigPath, IMcpWorkbenchService, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';
23
import { IMcpDiscovery } from './mcpDiscovery.js';
24
25
export class InstalledMcpServersDiscovery extends Disposable implements IMcpDiscovery {
26
27
readonly fromGallery = true;
28
private readonly collectionDisposables = this._register(new DisposableMap<string, IDisposable>());
29
30
constructor(
31
@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,
32
@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,
33
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
34
@ITextModelService private readonly textModelService: ITextModelService,
35
) {
36
super();
37
}
38
39
public start(): void {
40
const throttler = this._register(new Throttler());
41
this._register(this.mcpWorkbenchService.onChange(() => throttler.queue(() => this.sync())));
42
this.sync();
43
}
44
45
private async getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {
46
const store = new DisposableStore();
47
try {
48
const ref = await this.textModelService.createModelReference(resource);
49
store.add(ref);
50
const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });
51
return serverIdMapping;
52
} catch {
53
return new Map();
54
} finally {
55
store.dispose();
56
}
57
}
58
59
private async sync(): Promise<void> {
60
try {
61
const remoteEnv = await this.remoteAgentService.getEnvironment();
62
const collections = new Map<string, [IMcpConfigPath | undefined, McpServerDefinition[]]>();
63
const mcpConfigPathInfos = new ResourceMap<Promise<IMcpConfigPath & { locations: Map<string, Location> } | undefined>>();
64
for (const server of this.mcpWorkbenchService.getEnabledLocalMcpServers()) {
65
let mcpConfigPathPromise = mcpConfigPathInfos.get(server.mcpResource);
66
if (!mcpConfigPathPromise) {
67
mcpConfigPathPromise = (async (local: IWorkbenchLocalMcpServer) => {
68
const mcpConfigPath = this.mcpWorkbenchService.getMcpConfigPath(local);
69
const locations = mcpConfigPath?.uri ? await this.getServerIdMapping(mcpConfigPath?.uri, mcpConfigPath.section ? [...mcpConfigPath.section, 'servers'] : ['servers']) : new Map();
70
return mcpConfigPath ? { ...mcpConfigPath, locations } : undefined;
71
})(server);
72
mcpConfigPathInfos.set(server.mcpResource, mcpConfigPathPromise);
73
}
74
75
const config = server.config;
76
const mcpConfigPath = await mcpConfigPathPromise;
77
const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;
78
79
let definitions = collections.get(collectionId);
80
if (!definitions) {
81
definitions = [mcpConfigPath, []];
82
collections.set(collectionId, definitions);
83
}
84
85
const { isAbsolute, join, sep } = mcpConfigPath?.remoteAuthority && remoteEnv
86
? (remoteEnv.os === OperatingSystem.Windows ? pathWin32 : pathPosix)
87
: (isWindows ? pathWin32 : pathPosix);
88
const fsPathForRemote = (uri: URI) => {
89
const fsPathLocal = uri.fsPath;
90
return fsPathLocal.replaceAll(pathSep, sep);
91
};
92
93
const launch: McpServerLaunch = config.type === 'http' ? {
94
type: McpServerTransportType.HTTP,
95
uri: URI.parse(config.url),
96
headers: Object.entries(config.headers || {}),
97
} : {
98
type: McpServerTransportType.Stdio,
99
command: config.command,
100
args: config.args || [],
101
env: config.env || {},
102
envFile: config.envFile,
103
cwd: config.cwd
104
// if the cwd is defined in a workspace folder but not absolute (and not
105
// a variable or tilde-expansion) then resolve it in the workspace folder
106
// if the cwd is defined in a workspace folder but not absolute (and not
107
// a variable or tilde-expansion) then resolve it in the workspace folder
108
? (!isAbsolute(config.cwd) && !config.cwd.startsWith('~') && !config.cwd.startsWith('${') && mcpConfigPath?.workspaceFolder
109
? join(fsPathForRemote(mcpConfigPath.workspaceFolder.uri), config.cwd)
110
: config.cwd)
111
: mcpConfigPath?.workspaceFolder
112
? fsPathForRemote(mcpConfigPath.workspaceFolder.uri)
113
: undefined,
114
};
115
116
definitions[1].push({
117
id: `${collectionId}.${server.name}`,
118
label: server.name,
119
launch,
120
cacheNonce: await McpServerLaunch.hash(launch),
121
roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,
122
variableReplacement: {
123
folder: mcpConfigPath?.workspaceFolder,
124
section: mcpConfigurationSection,
125
target: mcpConfigPath?.target ?? ConfigurationTarget.USER,
126
},
127
devMode: config.dev,
128
presentation: {
129
order: mcpConfigPath?.order,
130
origin: mcpConfigPath?.locations.get(server.name)
131
}
132
});
133
}
134
135
for (const [id, [mcpConfigPath, serverDefinitions]] of collections) {
136
this.collectionDisposables.deleteAndDispose(id);
137
this.collectionDisposables.set(id, this.mcpRegistry.registerCollection({
138
id,
139
label: mcpConfigPath?.label ?? '',
140
presentation: {
141
order: serverDefinitions[0]?.presentation?.order,
142
origin: mcpConfigPath?.uri,
143
},
144
remoteAuthority: mcpConfigPath?.remoteAuthority ?? null,
145
serverDefinitions: observableValue(this, serverDefinitions),
146
trustBehavior: mcpConfigPath?.workspaceFolder ? McpServerTrust.Kind.TrustedOnNonce : McpServerTrust.Kind.Trusted,
147
configTarget: mcpConfigPath?.target ?? ConfigurationTarget.USER,
148
scope: mcpConfigPath?.scope ?? StorageScope.PROFILE,
149
}));
150
}
151
for (const [id] of this.collectionDisposables) {
152
if (!collections.has(id)) {
153
this.collectionDisposables.deleteAndDispose(id);
154
}
155
}
156
157
} catch (error) {
158
this.collectionDisposables.clearAndDisposeAll();
159
}
160
}
161
}
162
163