Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/common/mcpResourceScannerService.ts
3294 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 { assertNever } from '../../../base/common/assert.js';
7
import { Queue } from '../../../base/common/async.js';
8
import { VSBuffer } from '../../../base/common/buffer.js';
9
import { IStringDictionary } from '../../../base/common/collections.js';
10
import { parse, ParseError } from '../../../base/common/json.js';
11
import { Disposable } from '../../../base/common/lifecycle.js';
12
import { ResourceMap } from '../../../base/common/map.js';
13
import { Mutable } from '../../../base/common/types.js';
14
import { URI } from '../../../base/common/uri.js';
15
import { ConfigurationTarget, ConfigurationTargetToString } from '../../configuration/common/configuration.js';
16
import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
17
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
18
import { createDecorator } from '../../instantiation/common/instantiation.js';
19
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
20
import { IInstallableMcpServer } from './mcpManagement.js';
21
import { ICommonMcpServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js';
22
23
interface IScannedMcpServers {
24
servers?: IStringDictionary<Mutable<IMcpServerConfiguration>>;
25
inputs?: IMcpServerVariable[];
26
}
27
28
interface IOldScannedMcpServer {
29
id: string;
30
name: string;
31
version?: string;
32
gallery?: boolean;
33
config: Mutable<IMcpServerConfiguration>;
34
}
35
36
interface IScannedWorkspaceMcpServers {
37
settings?: {
38
mcp?: IScannedMcpServers;
39
};
40
}
41
42
export type McpResourceTarget = ConfigurationTarget.USER | ConfigurationTarget.WORKSPACE | ConfigurationTarget.WORKSPACE_FOLDER;
43
44
export const IMcpResourceScannerService = createDecorator<IMcpResourceScannerService>('IMcpResourceScannerService');
45
export interface IMcpResourceScannerService {
46
readonly _serviceBrand: undefined;
47
scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers>;
48
addMcpServers(servers: IInstallableMcpServer[], mcpResource: URI, target?: McpResourceTarget): Promise<void>;
49
removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void>;
50
}
51
52
export class McpResourceScannerService extends Disposable implements IMcpResourceScannerService {
53
readonly _serviceBrand: undefined;
54
55
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IScannedMcpServers>>();
56
57
constructor(
58
@IFileService private readonly fileService: IFileService,
59
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
60
) {
61
super();
62
}
63
64
async scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers> {
65
return this.withProfileMcpServers(mcpResource, target);
66
}
67
68
async addMcpServers(servers: IInstallableMcpServer[], mcpResource: URI, target?: McpResourceTarget): Promise<void> {
69
await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {
70
let updatedInputs = scannedMcpServers.inputs ?? [];
71
const existingServers = scannedMcpServers.servers ?? {};
72
for (const { name, config, inputs } of servers) {
73
existingServers[name] = config;
74
if (inputs) {
75
const existingInputIds = new Set(updatedInputs.map(input => input.id));
76
const newInputs = inputs.filter(input => !existingInputIds.has(input.id));
77
updatedInputs = [...updatedInputs, ...newInputs];
78
}
79
}
80
return { servers: existingServers, inputs: updatedInputs };
81
});
82
}
83
84
async removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void> {
85
await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {
86
for (const serverName of serverNames) {
87
if (scannedMcpServers.servers?.[serverName]) {
88
delete scannedMcpServers.servers[serverName];
89
}
90
}
91
return scannedMcpServers;
92
});
93
}
94
95
private async withProfileMcpServers(mcpResource: URI, target?: McpResourceTarget, updateFn?: (data: IScannedMcpServers) => IScannedMcpServers): Promise<IScannedMcpServers> {
96
return this.getResourceAccessQueue(mcpResource)
97
.queue(async (): Promise<IScannedMcpServers> => {
98
target = target ?? ConfigurationTarget.USER;
99
let scannedMcpServers: IScannedMcpServers = {};
100
try {
101
const content = await this.fileService.readFile(mcpResource);
102
const errors: ParseError[] = [];
103
const result = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true }) || {};
104
if (errors.length > 0) {
105
throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));
106
}
107
108
if (target === ConfigurationTarget.USER) {
109
scannedMcpServers = this.fromUserMcpServers(result);
110
} else if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
111
scannedMcpServers = this.fromWorkspaceFolderMcpServers(result);
112
} else if (target === ConfigurationTarget.WORKSPACE) {
113
const workspaceScannedMcpServers: IScannedWorkspaceMcpServers = result;
114
if (workspaceScannedMcpServers.settings?.mcp) {
115
scannedMcpServers = this.fromWorkspaceFolderMcpServers(workspaceScannedMcpServers.settings?.mcp);
116
}
117
}
118
} catch (error) {
119
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
120
throw error;
121
}
122
}
123
if (updateFn) {
124
scannedMcpServers = updateFn(scannedMcpServers ?? {});
125
126
if (target === ConfigurationTarget.USER) {
127
await this.writeScannedMcpServers(mcpResource, scannedMcpServers);
128
} else if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
129
await this.writeScannedMcpServersToWorkspaceFolder(mcpResource, scannedMcpServers);
130
} else if (target === ConfigurationTarget.WORKSPACE) {
131
await this.writeScannedMcpServersToWorkspace(mcpResource, scannedMcpServers);
132
} else {
133
assertNever(target, `Invalid Target: ${ConfigurationTargetToString(target)}`);
134
}
135
}
136
return scannedMcpServers;
137
});
138
}
139
140
private async writeScannedMcpServers(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
141
if ((scannedMcpServers.servers && Object.keys(scannedMcpServers.servers).length > 0) || (scannedMcpServers.inputs && scannedMcpServers.inputs.length > 0)) {
142
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedMcpServers, null, '\t')));
143
} else {
144
await this.fileService.del(mcpResource);
145
}
146
}
147
148
private async writeScannedMcpServersToWorkspaceFolder(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
149
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedMcpServers, null, '\t')));
150
}
151
152
private async writeScannedMcpServersToWorkspace(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {
153
let scannedWorkspaceMcpServers: IScannedWorkspaceMcpServers | undefined;
154
try {
155
const content = await this.fileService.readFile(mcpResource);
156
const errors: ParseError[] = [];
157
scannedWorkspaceMcpServers = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true }) as IScannedWorkspaceMcpServers;
158
if (errors.length > 0) {
159
throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));
160
}
161
} catch (error) {
162
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
163
throw error;
164
}
165
scannedWorkspaceMcpServers = { settings: {} };
166
}
167
if (!scannedWorkspaceMcpServers.settings) {
168
scannedWorkspaceMcpServers.settings = {};
169
}
170
scannedWorkspaceMcpServers.settings.mcp = scannedMcpServers;
171
await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedWorkspaceMcpServers, null, '\t')));
172
}
173
174
private fromUserMcpServers(scannedMcpServers: IScannedMcpServers): IScannedMcpServers {
175
const userMcpServers: IScannedMcpServers = {
176
inputs: scannedMcpServers.inputs
177
};
178
const servers = Object.entries(scannedMcpServers.servers ?? {});
179
if (servers.length > 0) {
180
userMcpServers.servers = {};
181
for (const [serverName, server] of servers) {
182
userMcpServers.servers[serverName] = this.sanitizeServer(server);
183
}
184
}
185
return userMcpServers;
186
}
187
188
private fromWorkspaceFolderMcpServers(scannedWorkspaceFolderMcpServers: IScannedMcpServers): IScannedMcpServers {
189
const scannedMcpServers: IScannedMcpServers = {
190
inputs: scannedWorkspaceFolderMcpServers.inputs
191
};
192
const servers = Object.entries(scannedWorkspaceFolderMcpServers.servers ?? {});
193
if (servers.length > 0) {
194
scannedMcpServers.servers = {};
195
for (const [serverName, config] of servers) {
196
scannedMcpServers.servers[serverName] = this.sanitizeServer(config);
197
}
198
}
199
return scannedMcpServers;
200
}
201
202
private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable<IMcpServerConfiguration>): IMcpServerConfiguration {
203
let server: IMcpServerConfiguration;
204
if ((<IOldScannedMcpServer>serverOrConfig).config) {
205
const oldScannedMcpServer = <IOldScannedMcpServer>serverOrConfig;
206
server = {
207
...oldScannedMcpServer.config,
208
version: oldScannedMcpServer.version,
209
gallery: oldScannedMcpServer.gallery
210
};
211
} else {
212
server = serverOrConfig as IMcpServerConfiguration;
213
}
214
215
if (server.type === undefined || (server.type !== McpServerType.REMOTE && server.type !== McpServerType.LOCAL)) {
216
(<Mutable<ICommonMcpServerConfiguration>>server).type = (<IMcpStdioServerConfiguration>server).command ? McpServerType.LOCAL : McpServerType.REMOTE;
217
}
218
219
return server;
220
}
221
222
private getResourceAccessQueue(file: URI): Queue<IScannedMcpServers> {
223
let resourceQueue = this.resourcesAccessQueueMap.get(file);
224
if (!resourceQueue) {
225
resourceQueue = new Queue<IScannedMcpServers>();
226
this.resourcesAccessQueueMap.set(file, resourceQueue);
227
}
228
return resourceQueue;
229
}
230
}
231
232
registerSingleton(IMcpResourceScannerService, McpResourceScannerService, InstantiationType.Delayed);
233
234