Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadMcp.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 { disposableTimeout } from '../../../base/common/async.js';
7
import { CancellationError } from '../../../base/common/errors.js';
8
import { Emitter } from '../../../base/common/event.js';
9
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
10
import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata } from '../../../base/common/oauth.js';
11
import { ISettableObservable, observableValue } from '../../../base/common/observable.js';
12
import Severity from '../../../base/common/severity.js';
13
import { URI, UriComponents } from '../../../base/common/uri.js';
14
import * as nls from '../../../nls.js';
15
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
16
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
17
import { LogLevel } from '../../../platform/log/common/log.js';
18
import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js';
19
import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../../contrib/mcp/common/mcpTypes.js';
20
import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js';
21
import { IAuthenticationMcpAccessService } from '../../services/authentication/browser/authenticationMcpAccessService.js';
22
import { IAuthenticationMcpService } from '../../services/authentication/browser/authenticationMcpService.js';
23
import { IAuthenticationMcpUsageService } from '../../services/authentication/browser/authenticationMcpUsageService.js';
24
import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationService } from '../../services/authentication/common/authentication.js';
25
import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js';
26
import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js';
27
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
28
import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js';
29
30
@extHostNamedCustomer(MainContext.MainThreadMcp)
31
export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
32
33
private _serverIdCounter = 0;
34
35
private readonly _servers = new Map<number, ExtHostMcpServerLaunch>();
36
private readonly _serverDefinitions = new Map<number, McpServerDefinition>();
37
private readonly _proxy: Proxied<ExtHostMcpShape>;
38
private readonly _collectionDefinitions = this._register(new DisposableMap<string, {
39
fromExtHost: McpCollectionDefinition.FromExtHost;
40
servers: ISettableObservable<readonly McpServerDefinition[]>;
41
dispose(): void;
42
}>());
43
44
constructor(
45
private readonly _extHostContext: IExtHostContext,
46
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
47
@IDialogService private readonly dialogService: IDialogService,
48
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
49
@IAuthenticationMcpService private readonly authenticationMcpServersService: IAuthenticationMcpService,
50
@IAuthenticationMcpAccessService private readonly authenticationMCPServerAccessService: IAuthenticationMcpAccessService,
51
@IAuthenticationMcpUsageService private readonly authenticationMCPServerUsageService: IAuthenticationMcpUsageService,
52
) {
53
super();
54
const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp);
55
this._register(this._mcpRegistry.registerDelegate({
56
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
57
priority: _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
58
waitForInitialProviderPromises() {
59
return proxy.$waitForInitialCollectionProviders();
60
},
61
canStart(collection, serverDefinition) {
62
if (collection.remoteAuthority !== _extHostContext.remoteAuthority) {
63
return false;
64
}
65
if (serverDefinition.launch.type === McpServerTransportType.Stdio && _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker) {
66
return false;
67
}
68
return true;
69
},
70
start: (_collection, serverDefiniton, resolveLaunch) => {
71
const id = ++this._serverIdCounter;
72
const launch = new ExtHostMcpServerLaunch(
73
_extHostContext.extensionHostKind,
74
() => proxy.$stopMcp(id),
75
msg => proxy.$sendMessage(id, JSON.stringify(msg)),
76
);
77
this._servers.set(id, launch);
78
this._serverDefinitions.set(id, serverDefiniton);
79
proxy.$startMcp(id, resolveLaunch);
80
81
return launch;
82
},
83
}));
84
}
85
86
$upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, serversDto: McpServerDefinition.Serialized[]): void {
87
const servers = serversDto.map(McpServerDefinition.fromSerialized);
88
const existing = this._collectionDefinitions.get(collection.id);
89
if (existing) {
90
existing.servers.set(servers, undefined);
91
} else {
92
const serverDefinitions = observableValue<readonly McpServerDefinition[]>('mcpServers', servers);
93
const handle = this._mcpRegistry.registerCollection({
94
...collection,
95
source: new ExtensionIdentifier(collection.extensionId),
96
resolveServerLanch: collection.canResolveLaunch ? (async def => {
97
const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label);
98
return r ? McpServerLaunch.fromSerialized(r) : undefined;
99
}) : undefined,
100
trustBehavior: collection.isTrustedByDefault ? McpServerTrust.Kind.Trusted : McpServerTrust.Kind.TrustedOnNonce,
101
remoteAuthority: this._extHostContext.remoteAuthority,
102
serverDefinitions,
103
});
104
105
this._collectionDefinitions.set(collection.id, {
106
fromExtHost: collection,
107
servers: serverDefinitions,
108
dispose: () => handle.dispose(),
109
});
110
}
111
}
112
113
$deleteMcpCollection(collectionId: string): void {
114
this._collectionDefinitions.deleteAndDispose(collectionId);
115
}
116
117
$onDidChangeState(id: number, update: McpConnectionState): void {
118
const server = this._servers.get(id);
119
if (!server) {
120
return;
121
}
122
123
server.state.set(update, undefined);
124
if (!McpConnectionState.isRunning(update)) {
125
server.dispose();
126
this._servers.delete(id);
127
this._serverDefinitions.delete(id);
128
}
129
}
130
131
$onDidPublishLog(id: number, level: LogLevel, log: string): void {
132
if (typeof level === 'string') {
133
level = LogLevel.Info;
134
log = level as unknown as string;
135
}
136
137
this._servers.get(id)?.pushLog(level, log);
138
}
139
140
$onDidReceiveMessage(id: number, message: string): void {
141
this._servers.get(id)?.pushMessage(message);
142
}
143
144
async $getTokenFromServerMetadata(id: number, authServerComponents: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined> {
145
const server = this._serverDefinitions.get(id);
146
if (!server) {
147
return undefined;
148
}
149
150
const authorizationServer = URI.revive(authServerComponents);
151
const scopesSupported = resourceMetadata?.scopes_supported || serverMetadata.scopes_supported || [];
152
let providerId = await this._authenticationService.getOrActivateProviderIdForServer(authorizationServer);
153
if (!providerId) {
154
const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, serverMetadata, resourceMetadata);
155
if (!provider) {
156
return undefined;
157
}
158
providerId = provider.id;
159
}
160
const sessions = await this._authenticationService.getSessions(providerId, scopesSupported, { authorizationServer: authorizationServer }, true);
161
const accountNamePreference = this.authenticationMcpServersService.getAccountPreference(server.id, providerId);
162
let matchingAccountPreferenceSession: AuthenticationSession | undefined;
163
if (accountNamePreference) {
164
matchingAccountPreferenceSession = sessions.find(session => session.account.label === accountNamePreference);
165
}
166
const provider = this._authenticationService.getProvider(providerId);
167
let session: AuthenticationSession;
168
if (sessions.length) {
169
// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.
170
if (matchingAccountPreferenceSession && this.authenticationMCPServerAccessService.isAccessAllowed(providerId, matchingAccountPreferenceSession.account.label, server.id)) {
171
this.authenticationMCPServerUsageService.addAccountUsage(providerId, matchingAccountPreferenceSession.account.label, scopesSupported, server.id, server.label);
172
return matchingAccountPreferenceSession.accessToken;
173
}
174
// If we only have one account for a single auth provider, lets just check if it's allowed and return it if it is.
175
if (!provider.supportsMultipleAccounts && this.authenticationMCPServerAccessService.isAccessAllowed(providerId, sessions[0].account.label, server.id)) {
176
this.authenticationMCPServerUsageService.addAccountUsage(providerId, sessions[0].account.label, scopesSupported, server.id, server.label);
177
return sessions[0].accessToken;
178
}
179
}
180
181
const isAllowed = await this.loginPrompt(server.label, provider.label, false);
182
if (!isAllowed) {
183
throw new Error('User did not consent to login.');
184
}
185
186
if (sessions.length) {
187
session = provider.supportsMultipleAccounts
188
? await this.authenticationMcpServersService.selectSession(providerId, server.id, server.label, scopesSupported, sessions)
189
: sessions[0];
190
}
191
else {
192
const accountToCreate: AuthenticationSessionAccount | undefined = matchingAccountPreferenceSession?.account;
193
do {
194
session = await this._authenticationService.createSession(
195
providerId,
196
scopesSupported,
197
{
198
activateImmediate: true,
199
account: accountToCreate,
200
authorizationServer
201
});
202
} while (
203
accountToCreate
204
&& accountToCreate.label !== session.account.label
205
&& !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label)
206
);
207
}
208
209
this.authenticationMCPServerAccessService.updateAllowedMcpServers(providerId, session.account.label, [{ id: server.id, name: server.label, allowed: true }]);
210
this.authenticationMcpServersService.updateAccountPreference(server.id, providerId, session.account);
211
this.authenticationMCPServerUsageService.addAccountUsage(providerId, session.account.label, scopesSupported, server.id, server.label);
212
return session.accessToken;
213
}
214
215
private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise<boolean> {
216
const result = await this.dialogService.prompt({
217
message: nls.localize('incorrectAccount', "Incorrect account detected"),
218
detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel),
219
type: Severity.Warning,
220
cancelButton: true,
221
buttons: [
222
{
223
label: nls.localize('keep', 'Keep {0}', chosenAccountLabel),
224
run: () => chosenAccountLabel
225
},
226
{
227
label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel),
228
run: () => requestedAccountLabel
229
}
230
],
231
});
232
233
if (!result.result) {
234
throw new CancellationError();
235
}
236
237
return result.result === chosenAccountLabel;
238
}
239
240
private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise<boolean> {
241
const message = recreatingSession
242
? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel)
243
: nls.localize('confirmLogin', "The MCP Server Definition '{0}' wants to authenticate to {1}.", mcpLabel, providerLabel);
244
245
const buttons: IPromptButton<boolean | undefined>[] = [
246
{
247
label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),
248
run() {
249
return true;
250
},
251
}
252
];
253
const { result } = await this.dialogService.prompt({
254
type: Severity.Info,
255
message,
256
buttons,
257
cancelButton: true,
258
});
259
260
return result ?? false;
261
}
262
263
override dispose(): void {
264
for (const server of this._servers.values()) {
265
server.extHostDispose();
266
}
267
this._servers.clear();
268
this._serverDefinitions.clear();
269
super.dispose();
270
}
271
}
272
273
274
class ExtHostMcpServerLaunch extends Disposable implements IMcpMessageTransport {
275
public readonly state = observableValue<McpConnectionState>('mcpServerState', { state: McpConnectionState.Kind.Starting });
276
277
private readonly _onDidLog = this._register(new Emitter<{ level: LogLevel; message: string }>());
278
public readonly onDidLog = this._onDidLog.event;
279
280
private readonly _onDidReceiveMessage = this._register(new Emitter<MCP.JSONRPCMessage>());
281
public readonly onDidReceiveMessage = this._onDidReceiveMessage.event;
282
283
pushLog(level: LogLevel, message: string): void {
284
this._onDidLog.fire({ message, level });
285
}
286
287
pushMessage(message: string): void {
288
let parsed: MCP.JSONRPCMessage | undefined;
289
try {
290
parsed = JSON.parse(message);
291
} catch (e) {
292
this.pushLog(LogLevel.Warning, `Failed to parse message: ${JSON.stringify(message)}`);
293
}
294
295
if (parsed) {
296
if (Array.isArray(parsed)) { // streamable HTTP supports batching
297
parsed.forEach(p => this._onDidReceiveMessage.fire(p));
298
} else {
299
this._onDidReceiveMessage.fire(parsed);
300
}
301
}
302
}
303
304
constructor(
305
extHostKind: ExtensionHostKind,
306
public readonly stop: () => void,
307
public readonly send: (message: MCP.JSONRPCMessage) => void,
308
) {
309
super();
310
311
this._register(disposableTimeout(() => {
312
this.pushLog(LogLevel.Info, `Starting server from ${extensionHostKindToString(extHostKind)} extension host`);
313
}));
314
}
315
316
public extHostDispose() {
317
if (McpConnectionState.isRunning(this.state.get())) {
318
this.pushLog(LogLevel.Warning, 'Extension host shut down, server will stop.');
319
this.state.set({ state: McpConnectionState.Kind.Stopped }, undefined);
320
}
321
this.dispose();
322
}
323
324
public override dispose(): void {
325
if (McpConnectionState.isRunning(this.state.get())) {
326
this.stop();
327
}
328
329
super.dispose();
330
}
331
}
332
333