Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.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 { RunOnceScheduler } from '../../../../../base/common/async.js';
7
import { VSBuffer } from '../../../../../base/common/buffer.js';
8
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { autorun, IObservable, IReader, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { localize } from '../../../../../nls.js';
13
import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { IFileService } from '../../../../../platform/files/common/files.js';
15
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { ILabelService } from '../../../../../platform/label/common/label.js';
17
import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js';
18
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
19
import { StorageScope } from '../../../../../platform/storage/common/storage.js';
20
import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
21
import { DiscoverySource, discoverySourceLabel, mcpDiscoverySection } from '../mcpConfiguration.js';
22
import { IMcpRegistry } from '../mcpRegistryTypes.js';
23
import { McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition, McpServerTrust } from '../mcpTypes.js';
24
import { IMcpDiscovery } from './mcpDiscovery.js';
25
import { ClaudeDesktopMpcDiscoveryAdapter, CursorDesktopMpcDiscoveryAdapter, NativeMpcDiscoveryAdapter, WindsurfDesktopMpcDiscoveryAdapter } from './nativeMcpDiscoveryAdapters.js';
26
27
export type WritableMcpCollectionDefinition = McpCollectionDefinition & { serverDefinitions: ISettableObservable<readonly McpServerDefinition[]> };
28
29
export abstract class FilesystemMcpDiscovery extends Disposable implements IMcpDiscovery {
30
31
readonly fromGallery: boolean = false;
32
33
protected readonly _fsDiscoveryEnabled: IObservable<{ [K in DiscoverySource]: boolean } | undefined>;
34
35
constructor(
36
@IConfigurationService configurationService: IConfigurationService,
37
@IFileService private readonly _fileService: IFileService,
38
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
39
) {
40
super();
41
42
this._fsDiscoveryEnabled = observableConfigValue(mcpDiscoverySection, undefined, configurationService);
43
}
44
45
protected _isDiscoveryEnabled(reader: IReader, discoverySource: DiscoverySource): boolean {
46
const fsDiscovery = this._fsDiscoveryEnabled.read(reader);
47
if (typeof fsDiscovery === 'boolean') {
48
return fsDiscovery; // old commands
49
}
50
if (discoverySource && fsDiscovery?.[discoverySource] === true) {
51
return true;
52
}
53
return false;
54
}
55
56
protected watchFile(
57
file: URI,
58
collection: WritableMcpCollectionDefinition,
59
discoverySource: DiscoverySource,
60
adaptFile: (contents: VSBuffer) => Promise<McpServerDefinition[] | undefined>,
61
): IDisposable {
62
const store = new DisposableStore();
63
const collectionRegistration = store.add(new MutableDisposable());
64
const updateFile = async () => {
65
let definitions: McpServerDefinition[] = [];
66
try {
67
const contents = await this._fileService.readFile(file);
68
definitions = await adaptFile(contents.value) || [];
69
} catch {
70
// ignored
71
}
72
if (!definitions.length) {
73
collectionRegistration.clear();
74
} else {
75
collection.serverDefinitions.set(definitions, undefined);
76
if (!collectionRegistration.value) {
77
collectionRegistration.value = this._mcpRegistry.registerCollection(collection);
78
}
79
}
80
};
81
82
store.add(autorun(reader => {
83
if (!this._isDiscoveryEnabled(reader, discoverySource)) {
84
collectionRegistration.clear();
85
return;
86
}
87
88
const throttler = reader.store.add(new RunOnceScheduler(updateFile, 500));
89
const watcher = reader.store.add(this._fileService.createWatcher(file, { recursive: false, excludes: [] }));
90
reader.store.add(watcher.onDidChange(() => throttler.schedule()));
91
updateFile();
92
}));
93
94
return store;
95
}
96
97
public abstract start(): void;
98
}
99
100
/**
101
* Base class that discovers MCP servers on a filesystem, outside of the ones
102
* defined in VS Code settings.
103
*/
104
export abstract class NativeFilesystemMcpDiscovery extends FilesystemMcpDiscovery implements IMcpDiscovery {
105
private readonly adapters: readonly NativeMpcDiscoveryAdapter[];
106
private suffix = '';
107
108
constructor(
109
remoteAuthority: string | null,
110
@ILabelService labelService: ILabelService,
111
@IFileService fileService: IFileService,
112
@IInstantiationService instantiationService: IInstantiationService,
113
@IMcpRegistry mcpRegistry: IMcpRegistry,
114
@IConfigurationService configurationService: IConfigurationService,
115
) {
116
super(configurationService, fileService, mcpRegistry);
117
if (remoteAuthority) {
118
this.suffix = ' ' + localize('onRemoteLabel', ' on {0}', labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority));
119
}
120
121
this.adapters = [
122
instantiationService.createInstance(ClaudeDesktopMpcDiscoveryAdapter, remoteAuthority),
123
instantiationService.createInstance(CursorDesktopMpcDiscoveryAdapter, remoteAuthority),
124
instantiationService.createInstance(WindsurfDesktopMpcDiscoveryAdapter, remoteAuthority),
125
];
126
}
127
128
protected setDetails(detailsDto: Dto<INativeMcpDiscoveryData> | undefined) {
129
if (!detailsDto) {
130
return;
131
}
132
133
const details: INativeMcpDiscoveryData = {
134
...detailsDto,
135
homedir: URI.revive(detailsDto.homedir),
136
xdgHome: detailsDto.xdgHome ? URI.revive(detailsDto.xdgHome) : undefined,
137
winAppData: detailsDto.winAppData ? URI.revive(detailsDto.winAppData) : undefined,
138
};
139
140
for (const adapter of this.adapters) {
141
const file = adapter.getFilePath(details);
142
if (!file) {
143
continue;
144
}
145
146
const collection: WritableMcpCollectionDefinition = {
147
id: adapter.id,
148
label: discoverySourceLabel[adapter.discoverySource] + this.suffix,
149
remoteAuthority: adapter.remoteAuthority,
150
configTarget: ConfigurationTarget.USER,
151
scope: StorageScope.PROFILE,
152
trustBehavior: McpServerTrust.Kind.TrustedOnNonce,
153
serverDefinitions: observableValue<readonly McpServerDefinition[]>(this, []),
154
presentation: {
155
origin: file,
156
order: adapter.order + (adapter.remoteAuthority ? McpCollectionSortOrder.RemoteBoost : 0),
157
},
158
};
159
160
this._register(this.watchFile(file, collection, adapter.discoverySource, contents => adapter.adaptFile(contents, details)));
161
}
162
}
163
}
164
165