src/vs/platform/mcp/common/mcpManagementService.ts

716 LOC · 544 covered · 172 uncovered · 138 ranges · 612 concepts · 44 introducers · 331 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mcpManagementService.ts ×36
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 { VSBuffer } from '../../../base/common/buffer.js';
8 > import { CancellationToken } from '../../../base/common/cancellation.js';
9 > import { Emitter, Event } from '../../../base/common/event.js';
10 > import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
11 > import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
12 > import { ResourceMap } from '../../../base/common/map.js';
13 > import { equals } from '../../../base/common/objects.js';
14 > import { isString } from '../../../base/common/types.js';
15 > import { URI } from '../../../base/common/uri.js';
16 > import { localize } from '../../../nls.js';
17 > import { ConfigurationTarget } from '../../configuration/common/configuration.js';
18 > import { IEnvironmentService } from '../../environment/common/environment.js';
19 > import { IFileService } from '../../files/common/files.js';
20 > import { IInstantiationService } from '../../instantiation/common/instantiation.js';
21 > import { ILogService } from '../../log/common/log.js';
22 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
23 > import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
24 > import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IGalleryMcpServerConfiguration, InstallMcpServerEvent, InstallMcpServerResult, RegistryType, UninstallMcpServerEvent, InstallOptions, UninstallOptions, IInstallableMcpServer, IAllowedMcpServersService, IMcpServerArgument, IMcpServerKeyValueInput, McpServerConfigurationParseResult } from './mcpManagement.js';
25 > import { IMcpSandboxConfiguration, IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js';
26 > import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js';
27 >
28 > export interface ILocalMcpServerInfo {
29 > name: string;
30 > version?: string;
31 > displayName?: string;
32 > galleryId?: string;
33 > galleryUrl?: string;
34 > description?: string;
35 > repositoryUrl?: string;
36 > publisher?: string;
37 > publisherDisplayName?: string;
38 > icon?: {
39 > dark: string;
40 > light: string;
41 > };
42 > codicon?: string;
43 > manifest?: IGalleryMcpServerConfiguration;
44 > readmeUrl?: URI;
45 > location?: URI;
46 > licenseUrl?: string;
47 > }
48 >
49 > export abstract class AbstractCommonMcpManagementService extends Disposable implements IMcpManagementService {
50 >
51 > _serviceBrand: undefined;
52 >
53 > abstract onInstallMcpServer: Event<InstallMcpServerEvent>;
54 > abstract onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
55 > abstract onDidUpdateMcpServers: Event<readonly InstallMcpServerResult[]>;
56 > abstract onUninstallMcpServer: Event<UninstallMcpServerEvent>;
57 > abstract onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
58 >
59 > abstract getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;
60 > abstract install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
61 > abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
62 > abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer>;
63 > abstract uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;
64 > abstract canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString;
65 >
66 > constructor(
67 > @ILogService protected readonly logService: ILogService mcpManagementService.ts ×1
68 > ) {
69 > super();
70 > }
72 > getMcpServerConfigurationFromManifest(manifest: IGalleryMcpServerConfiguration, packageType: RegistryType): McpServerConfigurationParseResult {
74 > // remote
75 > if (packageType === RegistryType.REMOTE && manifest.remotes?.length) {
76 > const url = manifest.remotes[0].url; mcpManagementService.ts ×1
77 > const headers = manifest.remotes[0].headers ?? [];
78 > const { inputs, variables } = this.processKeyValueInputs(url.startsWith('https://api.githubcopilot.com/mcp') ? headers.filter(h => h.name.toLowerCase() !== 'authorization') : headers);
79 > return {
80 > mcpServerConfiguration: {
81 > config: {
82 > type: McpServerType.REMOTE,
83 > url: manifest.remotes[0].url,
84 > headers: Object.keys(inputs).length ? inputs : undefined,
85 > },
86 > inputs: variables.length ? variables : undefined,
87 > },
88 > notices: [],
89 > };
90 > }
92 > // local
93 > const serverPackage = manifest.packages?.find(p => p.registryType === packageType) ?? manifest.packages?.[0]; mcpManagementService.ts ×10
94 > if (!serverPackage) {
95 > throw new Error(`No server package found`); mcpManagementService.ts ×1
96 > }
98 > const args: string[] = [];
99 > const inputs: IMcpServerVariable[] = [];
100 > const env: Record<string, string> = {};
101 > const notices: string[] = [];
102 >
103 > if (serverPackage.registryType === RegistryType.DOCKER) {
104 > args.push('run'); mcpManagementService.ts ×3
105 > args.push('-i');
106 > args.push('--rm');
107 > }
109 > if (serverPackage.runtimeArguments?.length) { mcpManagementService.ts ×10
110 > const result = this.processArguments(serverPackage.runtimeArguments ?? []); mcpManagementService.ts ×1
111 > args.push(...result.args);
112 > inputs.push(...result.variables);
113 > notices.push(...result.notices);
114 > }
116 > if (serverPackage.environmentVariables?.length) { mcpManagementService.ts ×10
117 > const { inputs: envInputs, variables: envVariables, notices: envNotices } = this.processKeyValueInputs(serverPackage.environmentVariables ?? []); mcpManagementService.ts ×2
118 > inputs.push(...envVariables);
119 > notices.push(...envNotices);
120 > for (const [name, value] of Object.entries(envInputs)) {
121 > env[name] = value;
122 > if (serverPackage.registryType === RegistryType.DOCKER) {
123 > args.push('-e'); mcpManagementService.ts ×1
124 > args.push(name);
125 > }
127 > }
129 > switch (serverPackage.registryType) {
130 > case RegistryType.NODE:
131 > if (serverPackage.registryBaseUrl) { mcpManagementService.ts ×2
132 > args.push('--registry', serverPackage.registryBaseUrl); mcpManagementService.ts ×1
133 > }
134 > args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); mcpManagementService.ts ×2
135 > break;
136 > case RegistryType.PYTHON: mcpManagementService.ts ×10
137 > if (serverPackage.registryBaseUrl) { mcpManagementService.ts ×2
138 > args.push('--index-url', serverPackage.registryBaseUrl); mcpManagementService.ts ×1
139 > }
140 > args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); mcpManagementService.ts ×2
141 > break;
142 > case RegistryType.DOCKER: mcpManagementService.ts ×10
144 > const dockerIdentifier = serverPackage.registryBaseUrl
145 > ? `${serverPackage.registryBaseUrl}/${serverPackage.identifier}` mcpManagementService.ts ×1
146 > : serverPackage.identifier; mcpManagementService.ts ×1
147 > args.push(serverPackage.version ? `${dockerIdentifier}:${serverPackage.version}` : dockerIdentifier); mcpManagementService.ts ×3
148 > break;
149 > }
150 > case RegistryType.NUGET: mcpManagementService.ts ×10
151 > args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier); mcpManagementService.ts ×3
152 > args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here
153 > if (serverPackage.registryBaseUrl) {
154 > args.push('--source', serverPackage.registryBaseUrl); mcpManagementService.ts ×1
155 > }
156 > if (serverPackage.packageArguments?.length) { mcpManagementService.ts ×3
157 > args.push('--'); mcpManagementService.ts ×1
158 > }
162 > if (serverPackage.packageArguments?.length) { mcpManagementService.ts ×10
163 > const result = this.processArguments(serverPackage.packageArguments); mcpManagementService.ts ×3
164 > args.push(...result.args);
165 > inputs.push(...result.variables);
166 > notices.push(...result.notices);
167 > }
169 > return {
170 > notices,
171 > mcpServerConfiguration: {
172 > config: {
173 > type: McpServerType.LOCAL,
174 > command: this.getCommandName(serverPackage.registryType),
175 > args: args.length ? args : undefined, mcpManagementService.ts ×10
176 > env: Object.keys(env).length ? env : undefined,
177 > },
178 > inputs: inputs.length ? inputs : undefined,
179 > }
180 > };
181 > }
183 > protected getCommandName(packageType: RegistryType): string {
184 > switch (packageType) { mcpManagementService.ts ×8
185 > case RegistryType.NODE: return 'npx';
186 > case RegistryType.DOCKER: return 'docker';
187 > case RegistryType.PYTHON: return 'uvx';
188 > case RegistryType.NUGET: return 'dnx';
189 > }
190 return packageType;
193 > protected getVariables(variableInputs: Record<string, IMcpServerInput>): IMcpServerVariable[] {
194 > const variables: IMcpServerVariable[] = []; mcpManagementService.ts ×1
195 > for (const [key, value] of Object.entries(variableInputs)) {
196 > variables.push({
197 > id: key,
198 > type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
199 > description: value.description ?? '',
200 > password: !!value.isSecret,
201 > default: value.default,
202 > options: value.choices,
203 > });
204 > }
205 > return variables;
206 > }
208 > private processKeyValueInputs(keyValueInputs: ReadonlyArray<IMcpServerKeyValueInput>): { inputs: Record<string, string>; variables: IMcpServerVariable[]; notices: string[] } {
209 > const notices: string[] = []; mcpManagementService.ts ×2
210 > const inputs: Record<string, string> = {};
211 > const variables: IMcpServerVariable[] = [];
212 >
213 > for (const input of keyValueInputs) {
214 > const inputVariables = input.variables ? this.getVariables(input.variables) : []; mcpManagementService.ts ×3
215 > let value = input.value || '';
216 >
217 > // If explicit variables exist, use them regardless of value
218 > if (inputVariables.length) {
219 > for (const variable of inputVariables) { mcpManagementService.ts ×1
220 > value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
221 > }
222 > variables.push(...inputVariables);
223 > } else if (!value && (input.description || input.choices || input.default !== undefined)) { mcpManagementService.ts ×3
224 > // Only create auto-generated input variable if no explicit variables and no value mcpManagementService.ts ×1
225 > variables.push({
226 > id: input.name,
227 > type: input.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
228 > description: input.description ?? '',
229 > password: !!input.isSecret,
230 > default: input.default,
231 > options: input.choices,
232 > });
233 > value = `\${input:${input.name}}`;
234 > }
236 > inputs[input.name] = value;
237 > }
239 > return { inputs, variables, notices };
240 > }
242 > private processArguments(argumentsList: readonly IMcpServerArgument[]): { args: string[]; variables: IMcpServerVariable[]; notices: string[] } {
243 > const args: string[] = []; mcpManagementService.ts ×3
244 > const variables: IMcpServerVariable[] = [];
245 > const notices: string[] = [];
246 > for (const arg of argumentsList) {
247 > const argVariables = arg.variables ? this.getVariables(arg.variables) : [];
248 >
249 > if (arg.type === 'positional') {
250 > let value = arg.value; mcpManagementService.ts ×3
251 > if (value) {
252 > for (const variable of argVariables) { mcpManagementService.ts ×2
253 > value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`); mcpManagementService.ts ×2
254 > }
255 > args.push(value); mcpManagementService.ts ×2
256 > if (argVariables.length) {
257 > variables.push(...argVariables); mcpManagementService.ts ×2
258 > }
259 > } else if (arg.valueHint && (arg.description || arg.default !== undefined)) { mcpManagementService.ts ×3
260 > // Create input variable for positional argument without value mcpManagementService.ts ×1
261 > variables.push({
262 > id: arg.valueHint,
263 > type: McpServerVariableType.PROMPT,
264 > description: arg.description ?? '',
265 > password: false,
266 > default: arg.default,
267 > });
268 > args.push(`\${input:${arg.valueHint}}`);
270 > // Fallback to value_hint as literal mcpManagementService.ts ×1
271 > args.push(arg.valueHint ?? '');
272 > }
273 > } else if (arg.type === 'named') { mcpManagementService.ts ×3
274 > if (!arg.name) { mcpManagementService.ts ×2
275 > notices.push(`Named argument is missing a name. ${JSON.stringify(arg)}`); mcpManagementService.ts ×1
276 > continue;
277 > }
278 > args.push(arg.name); mcpManagementService.ts ×2
279 > if (arg.value) {
280 > let value = arg.value; mcpManagementService.ts ×2
281 > for (const variable of argVariables) {
282 > value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`); mcpManagementService.ts ×2
283 > }
284 > args.push(value); mcpManagementService.ts ×2
285 > if (argVariables.length) {
286 > variables.push(...argVariables); mcpManagementService.ts ×2
287 > }
288 > } else if (arg.description || arg.default !== undefined) { mcpManagementService.ts ×2
289 > // Create input variable for named argument without value mcpManagementService.ts ×1
290 > const variableId = arg.name.replace(/^--?/, '');
291 > variables.push({
292 > id: variableId,
293 > type: McpServerVariableType.PROMPT,
294 > description: arg.description ?? '',
295 > password: false,
296 > default: arg.default,
297 > });
298 > args.push(`\${input:${variableId}}`);
299 > }
302 > return { args, variables, notices };
303 > }
305 > }
306 >
307 > export abstract class AbstractMcpResourceManagementService extends AbstractCommonMcpManagementService {
308 >
309 > private initializePromise: Promise<void> | undefined;
310 > private readonly reloadConfigurationScheduler: RunOnceScheduler;
311 > private local = new Map<string, ILocalMcpServer>();
312 >
313 > protected readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
314 > readonly onInstallMcpServer = this._onInstallMcpServer.event;
315 >
316 > protected readonly _onDidInstallMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());
317 > get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; }
318 >
319 > protected readonly _onDidUpdateMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());
320 > get onDidUpdateMcpServers() { return this._onDidUpdateMcpServers.event; }
321 >
322 > protected readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
323 > get onUninstallMcpServer() { return this._onUninstallMcpServer.event; }
324 >
325 > protected _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
326 > get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }
327 >
328 > constructor(
329 > protected readonly mcpResource: URI, mcpManagementService.ts ×8
330 > protected readonly target: McpResourceTarget,
331 > @IMcpGalleryService protected readonly mcpGalleryService: IMcpGalleryService,
332 > @IFileService protected readonly fileService: IFileService,
333 > @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
334 > @ILogService logService: ILogService,
335 > @IMcpResourceScannerService protected readonly mcpResourceScannerService: IMcpResourceScannerService,
336 > @IAllowedMcpServersService protected readonly allowedMcpServersService: IAllowedMcpServersService,
337 > ) {
338 > super(logService);
339 > this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.updateLocal(), 50));
340 > }
342 > /**
343 > * Enforces the enterprise allow/deny policy at the point of persistence. Called by every
344 > * install path (installable and each gallery override) against the fully resolved server
345 > * configuration, so a caller that goes straight to the management API cannot bypass the
346 > * `canInstall` UI check, and a gallery entry cannot slip through if its resolved command/URL
347 > * differs from the pre-resolution metadata.
348 > */
349 > protected ensureServerAllowed(server: IGalleryMcpServer | IInstallableMcpServer): void {
350 > const result = this.allowedMcpServersService.isAllowed(server); mcpManagementService.ts ×4
351 > if (result !== true) {
352 > throw new Error(result.value); mcpManagementService.ts ×1
353 > }
356 > private initialize(): Promise<void> {
357 > if (!this.initializePromise) { mcpManagementService.ts ×8
358 > this.initializePromise = (async () => {
359 > try {
360 > this.local = await this.populateLocalServers();
361 > } finally {
362 > this.startWatching();
363 > }
364 > })();
365 > }
366 > return this.initializePromise;
367 > }
369 > private async populateLocalServers(): Promise<Map<string, ILocalMcpServer>> {
370 > this.logService.trace('AbstractMcpResourceManagementService#populateLocalServers', this.mcpResource.toString()); mcpManagementService.ts ×8
371 > const local = new Map<string, ILocalMcpServer>();
372 > try {
373 > const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);
374 > if (scannedMcpServers.servers) {
375 > await Promise.allSettled(Object.entries(scannedMcpServers.servers).map(async ([name, scannedServer]) => { mcpManagementService.ts ×10
376 > const server = await this.scanLocalServer(name, scannedServer, scannedMcpServers.sandbox);
377 > local.set(name, server);
378 > }));
379 > }
380 > } catch (error) { mcpManagementService.ts ×8
381 this.logService.debug('Could not read user MCP servers:', error);
382 throw error;
383 }
384 > return local; mcpManagementService.ts ×8
385 > }
387 > private startWatching(): void {
388 > this._register(this.fileService.watch(this.mcpResource)); mcpManagementService.ts ×8
389 > this._register(this.fileService.onDidFilesChange(e => {
390 if (e.affects(this.mcpResource)) {
391 this.reloadConfigurationScheduler.schedule();
392 }
394 > }
396 > protected async updateLocal(): Promise<void> {
398 > const current = await this.populateLocalServers();
399 >
400 > const added: ILocalMcpServer[] = [];
401 > const updated: ILocalMcpServer[] = [];
402 > const removed = [...this.local.keys()].filter(name => !current.has(name));
403 >
404 > for (const server of removed) {
405 this.local.delete(server);
406 }
408 > for (const [name, server] of current) {
409 > const previous = this.local.get(name);
410 > if (previous) {
411 > if (!equals(previous, server)) { mcpManagementService.ts ×2
412 > updated.push(server);
413 > this.local.set(name, server);
414 > }
416 > added.push(server); mcpResourceScannerService.ts ×7
417 > this.local.set(name, server);
418 > }
420 >
421 > for (const server of removed) {
422 this.local.delete(server);
423 this._onDidUninstallMcpServer.fire({ name: server, mcpResource: this.mcpResource });
424 }
426 > if (updated.length) {
427 > this._onDidUpdateMcpServers.fire(updated.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource }))); mcpManagementService.ts ×2
428 > }
430 > if (added.length) {
431 > this._onDidInstallMcpServers.fire(added.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource }))); mcpResourceScannerService.ts ×7
432 > }
434 > } catch (error) {
435 this.logService.error('Failed to load installed MCP servers:', error);
436 }
439 > async getInstalled(): Promise<ILocalMcpServer[]> {
440 > await this.initialize(); mcpManagementService.ts ×8
441 > return Array.from(this.local.values());
442 > }
444 > protected async scanLocalServer(name: string, config: IMcpServerConfiguration, rootSandbox?: IMcpSandboxConfiguration): Promise<ILocalMcpServer> {
445 > let mcpServerInfo = await this.getLocalServerInfo(name, config); mcpManagementService.ts ×10
446 > if (!mcpServerInfo) {
447 > mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined };
448 > }
449 >
450 > return {
451 > name,
452 > config,
453 > rootSandbox,
454 > mcpResource: this.mcpResource,
455 > version: mcpServerInfo.version,
456 > location: mcpServerInfo.location,
457 > displayName: mcpServerInfo.displayName,
458 > description: mcpServerInfo.description,
459 > publisher: mcpServerInfo.publisher,
460 > publisherDisplayName: mcpServerInfo.publisherDisplayName,
461 > galleryUrl: mcpServerInfo.galleryUrl,
462 > galleryId: mcpServerInfo.galleryId,
463 > repositoryUrl: mcpServerInfo.repositoryUrl,
464 > readmeUrl: mcpServerInfo.readmeUrl,
465 > icon: mcpServerInfo.icon,
466 > codicon: mcpServerInfo.codicon,
467 > manifest: mcpServerInfo.manifest,
468 > source: config.gallery ? 'gallery' : 'local'
469 > };
470 > }
472 > async install(server: IInstallableMcpServer, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {
473 > this.logService.trace('MCP Management Service: install', server.name); mcpManagementService.ts ×4
474 > this.ensureServerAllowed(server);
475 >
476 > this._onInstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });
477 > try {
478 > await this.mcpResourceScannerService.addMcpServers([server], this.mcpResource, this.target);
479 > await this.updateLocal(); mcpResourceScannerService.ts ×7
480 > const local = this.local.get(server.name);
481 > if (!local) {
482 throw new Error(`Failed to install MCP server: ${server.name}`);
483 }
484 > return local; mcpResourceScannerService.ts ×7
485 > } catch (e) {
486 this._onDidInstallMcpServers.fire([{ name: server.name, error: e, mcpResource: this.mcpResource }]);
487 throw e;
488 }
491 > async uninstall(server: ILocalMcpServer, options?: Omit<UninstallOptions, 'mcpResource'>): Promise<void> {
492 this.logService.trace('MCP Management Service: uninstall', server.name);
493 this._onUninstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });
494
495 try {
496 const currentServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);
497 if (!currentServers.servers) {
498 return;
499 }
500 await this.mcpResourceScannerService.removeMcpServers([server.name], this.mcpResource, this.target);
501 if (server.location) {
502 await this.fileService.del(URI.revive(server.location), { recursive: true });
503 }
504 await this.updateLocal();
505 } catch (e) {
506 this._onDidUninstallMcpServer.fire({ name: server.name, error: e, mcpResource: this.mcpResource });
507 throw e;
508 }
509 }
511 > protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined>;
512 > protected abstract installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer>;
513 > }
514 >
515 > export class McpUserResourceManagementService extends AbstractMcpResourceManagementService {
516 >
517 > protected readonly mcpLocation: URI;
518 >
519 > constructor(
520 mcpResource: URI,
521 @IMcpGalleryService mcpGalleryService: IMcpGalleryService,
522 @IFileService fileService: IFileService,
523 @IUriIdentityService uriIdentityService: IUriIdentityService,
524 @ILogService logService: ILogService,
525 @IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService,
526 @IAllowedMcpServersService allowedMcpServersService: IAllowedMcpServersService,
527 @IEnvironmentService environmentService: IEnvironmentService
528 ) {
529 super(mcpResource, ConfigurationTarget.USER, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService, allowedMcpServersService);
530 this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp');
531 }
533 > async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
534 throw new Error('Not supported');
535 }
537 > async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer): Promise<ILocalMcpServer> {
538 await this.updateMetadataFromGallery(gallery);
539 await this.updateLocal();
540 const updatedLocal = (await this.getInstalled()).find(s => s.name === local.name);
541 if (!updatedLocal) {
542 throw new Error(`Failed to find MCP server: ${local.name}`);
543 }
544 return updatedLocal;
545 }
547 > protected async updateMetadataFromGallery(gallery: IGalleryMcpServer): Promise<IGalleryMcpServerConfiguration> {
548 const manifest = gallery.configuration;
549 const location = this.getLocation(gallery.name, gallery.version);
550 const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
551 const local: ILocalMcpServerInfo = {
552 galleryUrl: gallery.galleryUrl,
553 galleryId: gallery.id,
554 name: gallery.name,
555 displayName: gallery.displayName,
556 description: gallery.description,
557 version: gallery.version,
558 publisher: gallery.publisher,
559 publisherDisplayName: gallery.publisherDisplayName,
560 repositoryUrl: gallery.repositoryUrl,
561 licenseUrl: gallery.license,
562 icon: gallery.icon,
563 codicon: gallery.codicon,
564 manifest,
565 };
566 await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify(local)));
567
568 if (gallery.readmeUrl || gallery.readme) {
569 const readme = gallery.readme ? gallery.readme : await this.mcpGalleryService.getReadme(gallery, CancellationToken.None);
570 await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
571 }
572
573 return manifest;
574 }
576 > protected async getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined> {
577 let storedMcpServerInfo: ILocalMcpServerInfo | undefined;
578 let location: URI | undefined;
579 let readmeUrl: URI | undefined;
580 if (mcpServerConfig.gallery) {
581 location = this.getLocation(name, mcpServerConfig.version);
582 const manifestLocation = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
583 try {
584 const content = await this.fileService.readFile(manifestLocation);
585 storedMcpServerInfo = JSON.parse(content.value.toString()) as ILocalMcpServerInfo;
586
587 // migrate
588 if (storedMcpServerInfo.galleryUrl?.includes('/v0/')) {
589 storedMcpServerInfo.galleryUrl = storedMcpServerInfo.galleryUrl.substring(0, storedMcpServerInfo.galleryUrl.indexOf('/v0/'));
590 await this.fileService.writeFile(manifestLocation, VSBuffer.fromString(JSON.stringify(storedMcpServerInfo)));
591 }
592
593 storedMcpServerInfo.location = location;
594 readmeUrl = this.uriIdentityService.extUri.joinPath(location, 'README.md');
595 if (!await this.fileService.exists(readmeUrl)) {
596 readmeUrl = undefined;
597 }
598 storedMcpServerInfo.readmeUrl = readmeUrl;
599 } catch (e) {
600 this.logService.error('MCP Management Service: failed to read manifest', location.toString(), e);
601 }
602 }
603 return storedMcpServerInfo;
604 }
606 > protected getLocation(name: string, version?: string): URI {
607 name = name.replace('/', '.');
608 return this.uriIdentityService.extUri.joinPath(this.mcpLocation, version ? `${name}-${version}` : name);
609 }
611 > protected override installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {
612 throw new Error('Method not supported.');
613 }
615 > override canInstall(): true | IMarkdownString {
616 throw new Error('Not supported');
617 }
619 > }
620 >
621 > export abstract class AbstractMcpManagementService extends AbstractCommonMcpManagementService implements IMcpManagementService {
622 >
623 > constructor(
624 @IAllowedMcpServersService protected readonly allowedMcpServersService: IAllowedMcpServersService,
625 @ILogService logService: ILogService,
626 ) {
627 super(logService);
628 }
630 > canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString {
631 const allowedToInstall = this.allowedMcpServersService.isAllowed(server);
632 if (allowedToInstall !== true) {
633 return new MarkdownString(localize('not allowed to install', "This mcp server cannot be installed because {0}", allowedToInstall.value));
634 }
635 return true;
636 }
638 >
639 > export class McpManagementService extends AbstractMcpManagementService implements IMcpManagementService {
640 >
641 > private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
642 > readonly onInstallMcpServer = this._onInstallMcpServer.event;
643 >
644 > private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
645 > readonly onDidInstallMcpServers = this._onDidInstallMcpServers.event;
646 >
647 > private readonly _onDidUpdateMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
648 > readonly onDidUpdateMcpServers = this._onDidUpdateMcpServers.event;
649 >
650 > private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
651 > readonly onUninstallMcpServer = this._onUninstallMcpServer.event;
652 >
653 > private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
654 > readonly onDidUninstallMcpServer = this._onDidUninstallMcpServer.event;
655 >
656 > private readonly mcpResourceManagementServices = new ResourceMap<{ service: McpUserResourceManagementService } & IDisposable>();
657 >
658 > constructor(
659 @IAllowedMcpServersService allowedMcpServersService: IAllowedMcpServersService,
660 @ILogService logService: ILogService,
661 @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
662 @IInstantiationService protected readonly instantiationService: IInstantiationService,
663 ) {
664 super(allowedMcpServersService, logService);
665 }
667 > private getMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {
668 let mcpResourceManagementService = this.mcpResourceManagementServices.get(mcpResource);
669 if (!mcpResourceManagementService) {
670 const disposables = new DisposableStore();
671 const service = disposables.add(this.createMcpResourceManagementService(mcpResource));
672 disposables.add(service.onInstallMcpServer(e => this._onInstallMcpServer.fire(e)));
673 disposables.add(service.onDidInstallMcpServers(e => this._onDidInstallMcpServers.fire(e)));
674 disposables.add(service.onDidUpdateMcpServers(e => this._onDidUpdateMcpServers.fire(e)));
675 disposables.add(service.onUninstallMcpServer(e => this._onUninstallMcpServer.fire(e)));
676 disposables.add(service.onDidUninstallMcpServer(e => this._onDidUninstallMcpServer.fire(e)));
677 this.mcpResourceManagementServices.set(mcpResource, mcpResourceManagementService = { service, dispose: () => disposables.dispose() });
678 }
679 return mcpResourceManagementService.service;
680 }
682 > async getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
683 const mcpResourceUri = mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
684 return this.getMcpResourceManagementService(mcpResourceUri).getInstalled();
685 }
687 > async install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
688 const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
689 return this.getMcpResourceManagementService(mcpResourceUri).install(server, options);
690 }
692 > async uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
693 const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
694 return this.getMcpResourceManagementService(mcpResourceUri).uninstall(server, options);
695 }
697 > async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
698 const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
699 return this.getMcpResourceManagementService(mcpResourceUri).installFromGallery(server, options);
700 }
702 > async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise<ILocalMcpServer> {
703 return this.getMcpResourceManagementService(mcpResource || this.userDataProfilesService.defaultProfile.mcpResource).updateMetadata(local, gallery);
704 }
706 > override dispose(): void {
707 this.mcpResourceManagementServices.forEach(service => service.dispose());
708 this.mcpResourceManagementServices.clear();
709 super.dispose();
710 }
712 > protected createMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {
713 return this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource);
714 }
716 > }