Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.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 { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
7
import { observableValue } from '../../../../../base/common/observable.js';
8
import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js';
9
import { localize } from '../../../../../nls.js';
10
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
11
import { IMcpCollectionContribution } from '../../../../../platform/extensions/common/extensions.js';
12
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
13
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
14
import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';
15
import { mcpActivationEvent, mcpContributionPoint } from '../mcpConfiguration.js';
16
import { IMcpRegistry } from '../mcpRegistryTypes.js';
17
import { extensionPrefixedIdentifier, McpServerDefinition, McpServerTrust } from '../mcpTypes.js';
18
import { IMcpDiscovery } from './mcpDiscovery.js';
19
20
const cacheKey = 'mcp.extCachedServers';
21
22
interface IServerCacheEntry {
23
readonly servers: readonly McpServerDefinition.Serialized[];
24
}
25
26
const _mcpExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint(mcpContributionPoint);
27
28
const enum PersistWhen {
29
CollectionExists,
30
Always,
31
}
32
33
export class ExtensionMcpDiscovery extends Disposable implements IMcpDiscovery {
34
35
readonly fromGallery = false;
36
37
private readonly _extensionCollectionIdsToPersist = new Map<string, PersistWhen>();
38
private readonly cachedServers: { [collcetionId: string]: IServerCacheEntry };
39
40
constructor(
41
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
42
@IStorageService storageService: IStorageService,
43
@IExtensionService private readonly _extensionService: IExtensionService,
44
) {
45
super();
46
this.cachedServers = storageService.getObject(cacheKey, StorageScope.WORKSPACE, {});
47
48
this._register(storageService.onWillSaveState(() => {
49
let updated = false;
50
for (const [collectionId, behavior] of this._extensionCollectionIdsToPersist.entries()) {
51
const collection = this._mcpRegistry.collections.get().find(c => c.id === collectionId);
52
let defs = collection?.serverDefinitions.get();
53
if (!collection || collection.lazy) {
54
if (behavior === PersistWhen.Always) {
55
defs = [];
56
} else {
57
continue;
58
}
59
}
60
61
if (defs) {
62
updated = true;
63
this.cachedServers[collectionId] = { servers: defs.map(McpServerDefinition.toSerialized) };
64
}
65
}
66
67
if (updated) {
68
storageService.store(cacheKey, this.cachedServers, StorageScope.WORKSPACE, StorageTarget.MACHINE);
69
}
70
}));
71
}
72
73
public start(): void {
74
const extensionCollections = this._register(new DisposableMap<string>());
75
this._register(_mcpExtensionPoint.setHandler((_extensions, delta) => {
76
const { added, removed } = delta;
77
78
for (const collections of removed) {
79
for (const coll of collections.value) {
80
extensionCollections.deleteAndDispose(extensionPrefixedIdentifier(collections.description.identifier, coll.id));
81
}
82
}
83
84
for (const collections of added) {
85
86
if (!ExtensionMcpDiscovery._validate(collections)) {
87
continue;
88
}
89
90
for (const coll of collections.value) {
91
const id = extensionPrefixedIdentifier(collections.description.identifier, coll.id);
92
this._extensionCollectionIdsToPersist.set(id, PersistWhen.CollectionExists);
93
94
const serverDefs = this.cachedServers.hasOwnProperty(id) ? this.cachedServers[id].servers : undefined;
95
const dispo = this._mcpRegistry.registerCollection({
96
id,
97
label: coll.label,
98
remoteAuthority: null,
99
trustBehavior: McpServerTrust.Kind.Trusted,
100
scope: StorageScope.WORKSPACE,
101
configTarget: ConfigurationTarget.USER,
102
serverDefinitions: observableValue<McpServerDefinition[]>(this, serverDefs?.map(McpServerDefinition.fromSerialized) || []),
103
lazy: {
104
isCached: !!serverDefs,
105
load: () => this._activateExtensionServers(coll.id).then(() => {
106
// persist (an empty collection) in case the extension doesn't end up publishing one
107
this._extensionCollectionIdsToPersist.set(id, PersistWhen.Always);
108
}),
109
removed: () => extensionCollections.deleteAndDispose(id),
110
},
111
source: collections.description.identifier
112
});
113
114
extensionCollections.set(id, dispo);
115
}
116
}
117
}));
118
}
119
120
private async _activateExtensionServers(collectionId: string): Promise<void> {
121
await this._extensionService.activateByEvent(mcpActivationEvent(collectionId));
122
await Promise.all(this._mcpRegistry.delegates.get()
123
.map(r => r.waitForInitialProviderPromises()));
124
}
125
126
private static _validate(user: extensionsRegistry.IExtensionPointUser<IMcpCollectionContribution[]>): boolean {
127
128
if (!Array.isArray(user.value)) {
129
user.collector.error(localize('invalidData', "Expected an array of MCP collections"));
130
return false;
131
}
132
133
for (const contribution of user.value) {
134
if (typeof contribution.id !== 'string' || isFalsyOrWhitespace(contribution.id)) {
135
user.collector.error(localize('invalidId', "Expected 'id' to be a non-empty string."));
136
return false;
137
}
138
if (typeof contribution.label !== 'string' || isFalsyOrWhitespace(contribution.label)) {
139
user.collector.error(localize('invalidLabel', "Expected 'label' to be a non-empty string."));
140
return false;
141
}
142
}
143
144
return true;
145
}
146
}
147
148