Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadMcp.ts
5237 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 { mapFindFirst } from '../../../base/common/arraysFind.js';
7
import { disposableTimeout, RunOnceScheduler } from '../../../base/common/async.js';
8
import { CancellationError } from '../../../base/common/errors.js';
9
import { Emitter } from '../../../base/common/event.js';
10
import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js';
11
import { autorun, ISettableObservable, observableValue } from '../../../base/common/observable.js';
12
import Severity from '../../../base/common/severity.js';
13
import { URI } from '../../../base/common/uri.js';
14
import { generateUuid } from '../../../base/common/uuid.js';
15
import * as nls from '../../../nls.js';
16
import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
17
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
18
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
19
import { LogLevel } from '../../../platform/log/common/log.js';
20
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';
21
import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../../contrib/mcp/common/mcpGatewayService.js';
22
import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js';
23
import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js';
24
import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js';
25
import { IAuthenticationMcpAccessService } from '../../services/authentication/browser/authenticationMcpAccessService.js';
26
import { IAuthenticationMcpService } from '../../services/authentication/browser/authenticationMcpService.js';
27
import { IAuthenticationMcpUsageService } from '../../services/authentication/browser/authenticationMcpUsageService.js';
28
import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationService } from '../../services/authentication/common/authentication.js';
29
import { IDynamicAuthenticationProviderStorageService } from '../../services/authentication/common/dynamicAuthenticationProviderStorage.js';
30
import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js';
31
import { IExtensionService } from '../../services/extensions/common/extensions.js';
32
import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js';
33
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
34
import { ExtHostContext, ExtHostMcpShape, IMcpAuthenticationDetails, IMcpAuthenticationOptions, IAuthMetadataSource, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js';
35
36
@extHostNamedCustomer(MainContext.MainThreadMcp)
37
export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
38
39
private _serverIdCounter = 0;
40
41
private readonly _servers = new Map<number, ExtHostMcpServerLaunch>();
42
private readonly _serverDefinitions = new Map<number, McpServerDefinition>();
43
private readonly _serverAuthTracking = new McpServerAuthTracker();
44
private readonly _proxy: Proxied<ExtHostMcpShape>;
45
private readonly _collectionDefinitions = this._register(new DisposableMap<string, {
46
servers: ISettableObservable<readonly McpServerDefinition[]>;
47
dispose(): void;
48
}>());
49
private readonly _gateways = this._register(new DisposableMap<string, IMcpGatewayResult>());
50
51
constructor(
52
private readonly _extHostContext: IExtHostContext,
53
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
54
@IDialogService private readonly dialogService: IDialogService,
55
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
56
@IAuthenticationMcpService private readonly authenticationMcpServersService: IAuthenticationMcpService,
57
@IAuthenticationMcpAccessService private readonly authenticationMCPServerAccessService: IAuthenticationMcpAccessService,
58
@IAuthenticationMcpUsageService private readonly authenticationMCPServerUsageService: IAuthenticationMcpUsageService,
59
@IDynamicAuthenticationProviderStorageService private readonly _dynamicAuthenticationProviderStorageService: IDynamicAuthenticationProviderStorageService,
60
@IExtensionService private readonly _extensionService: IExtensionService,
61
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
62
@ITelemetryService private readonly _telemetryService: ITelemetryService,
63
@IWorkbenchMcpGatewayService private readonly _mcpGatewayService: IWorkbenchMcpGatewayService,
64
) {
65
super();
66
this._register(_authenticationService.onDidChangeSessions(e => this._onDidChangeAuthSessions(e.providerId, e.label)));
67
const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp);
68
this._register(this._mcpRegistry.registerDelegate({
69
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
70
priority: _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
71
waitForInitialProviderPromises() {
72
return proxy.$waitForInitialCollectionProviders();
73
},
74
canStart(collection, serverDefinition) {
75
if (collection.remoteAuthority !== _extHostContext.remoteAuthority) {
76
return false;
77
}
78
if (serverDefinition.launch.type === McpServerTransportType.Stdio && _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker) {
79
return false;
80
}
81
return true;
82
},
83
async substituteVariables(serverDefinition, launch) {
84
const ser = await proxy.$substituteVariables(serverDefinition.variableReplacement?.folder?.uri, McpServerLaunch.toSerialized(launch));
85
return McpServerLaunch.fromSerialized(ser);
86
},
87
start: (_collection, serverDefiniton, resolveLaunch, options) => {
88
const id = ++this._serverIdCounter;
89
const launch = new ExtHostMcpServerLaunch(
90
_extHostContext.extensionHostKind,
91
() => proxy.$stopMcp(id),
92
msg => proxy.$sendMessage(id, JSON.stringify(msg)),
93
);
94
this._servers.set(id, launch);
95
this._serverDefinitions.set(id, serverDefiniton);
96
proxy.$startMcp(id, {
97
launch: resolveLaunch,
98
defaultCwd: serverDefiniton.variableReplacement?.folder?.uri,
99
errorOnUserInteraction: options?.errorOnUserInteraction,
100
});
101
102
return launch;
103
},
104
}));
105
106
// Subscribe to MCP server definition changes and notify ext host
107
const onDidChangeMcpServerDefinitionsTrigger = this._register(new RunOnceScheduler(() => this._publishServerDefinitions(), 500));
108
this._register(autorun(reader => {
109
const collections = this._mcpRegistry.collections.read(reader);
110
// Read all server definitions to track changes
111
for (const collection of collections) {
112
collection.serverDefinitions.read(reader);
113
}
114
// Notify ext host that definitions changed (it will re-fetch if needed)
115
if (!onDidChangeMcpServerDefinitionsTrigger.isScheduled()) {
116
onDidChangeMcpServerDefinitionsTrigger.schedule();
117
}
118
}));
119
120
onDidChangeMcpServerDefinitionsTrigger.schedule();
121
}
122
123
private _publishServerDefinitions() {
124
const collections = this._mcpRegistry.collections.get();
125
const allServers: McpServerDefinition.Serialized[] = [];
126
127
for (const collection of collections) {
128
const servers = collection.serverDefinitions.get();
129
for (const server of servers) {
130
allServers.push(McpServerDefinition.toSerialized(server));
131
}
132
}
133
134
this._proxy.$onDidChangeMcpServerDefinitions(allServers);
135
}
136
137
$upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, serversDto: McpServerDefinition.Serialized[]): void {
138
const servers = serversDto.map(McpServerDefinition.fromSerialized);
139
const existing = this._collectionDefinitions.get(collection.id);
140
if (existing) {
141
existing.servers.set(servers, undefined);
142
} else {
143
const serverDefinitions = observableValue<readonly McpServerDefinition[]>('mcpServers', servers);
144
const extensionId = new ExtensionIdentifier(collection.extensionId);
145
const store = new DisposableStore();
146
const handle = store.add(new MutableDisposable());
147
const register = () => {
148
handle.value ??= this._mcpRegistry.registerCollection({
149
...collection,
150
source: extensionId,
151
resolveServerLanch: collection.canResolveLaunch ? (async def => {
152
const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label);
153
return r ? McpServerLaunch.fromSerialized(r) : undefined;
154
}) : undefined,
155
trustBehavior: collection.isTrustedByDefault ? McpServerTrust.Kind.Trusted : McpServerTrust.Kind.TrustedOnNonce,
156
remoteAuthority: this._extHostContext.remoteAuthority,
157
serverDefinitions,
158
});
159
};
160
161
const whenClauseStr = mapFindFirst(this._extensionService.extensions, e =>
162
ExtensionIdentifier.equals(extensionId, e.identifier)
163
? e.contributes?.mcpServerDefinitionProviders?.find(p => extensionPrefixedIdentifier(extensionId, p.id) === collection.id)?.when
164
: undefined);
165
const whenClause = whenClauseStr && ContextKeyExpr.deserialize(whenClauseStr);
166
167
if (!whenClause) {
168
register();
169
} else {
170
const evaluate = () => {
171
if (this._contextKeyService.contextMatchesRules(whenClause)) {
172
register();
173
} else {
174
handle.clear();
175
}
176
};
177
178
store.add(this._contextKeyService.onDidChangeContext(evaluate));
179
evaluate();
180
}
181
182
this._collectionDefinitions.set(collection.id, {
183
servers: serverDefinitions,
184
dispose: () => store.dispose(),
185
});
186
}
187
}
188
189
$deleteMcpCollection(collectionId: string): void {
190
this._collectionDefinitions.deleteAndDispose(collectionId);
191
}
192
193
$onDidChangeState(id: number, update: McpConnectionState): void {
194
const server = this._servers.get(id);
195
if (!server) {
196
return;
197
}
198
199
server.state.set(update, undefined);
200
if (!McpConnectionState.isRunning(update)) {
201
server.dispose();
202
this._servers.delete(id);
203
this._serverDefinitions.delete(id);
204
this._serverAuthTracking.untrack(id);
205
}
206
}
207
208
$onDidPublishLog(id: number, level: LogLevel, log: string): void {
209
if (typeof level === 'string') {
210
level = LogLevel.Info;
211
log = level as unknown as string;
212
}
213
214
this._servers.get(id)?.pushLog(level, log);
215
}
216
217
$onDidReceiveMessage(id: number, message: string): void {
218
this._servers.get(id)?.pushMessage(message);
219
}
220
221
async $getTokenForProviderId(id: number, providerId: string, scopes: string[], options: IMcpAuthenticationOptions = {}): Promise<string | undefined> {
222
const server = this._serverDefinitions.get(id);
223
if (!server) {
224
return undefined;
225
}
226
return this._getSessionForProvider(id, server, providerId, scopes, undefined, options.errorOnUserInteraction);
227
}
228
229
async $getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, { errorOnUserInteraction, forceNewRegistration }: IMcpAuthenticationOptions = {}): Promise<string | undefined> {
230
const server = this._serverDefinitions.get(id);
231
if (!server) {
232
return undefined;
233
}
234
const authorizationServer = URI.revive(authDetails.authorizationServer);
235
const resourceServer = authDetails.resourceMetadata?.resource ? URI.parse(authDetails.resourceMetadata.resource) : undefined;
236
const resolvedScopes = authDetails.scopes ?? authDetails.resourceMetadata?.scopes_supported ?? authDetails.authorizationServerMetadata.scopes_supported ?? [];
237
let providerId = await this._authenticationService.getOrActivateProviderIdForServer(authorizationServer, resourceServer);
238
if (forceNewRegistration && providerId) {
239
if (!this._authenticationService.isDynamicAuthenticationProvider(providerId)) {
240
throw new Error('Cannot force new registration for a non-dynamic authentication provider.');
241
}
242
this._authenticationService.unregisterAuthenticationProvider(providerId);
243
// TODO: Encapsulate this and the unregister in one call in the auth service
244
await this._dynamicAuthenticationProviderStorageService.removeDynamicProvider(providerId);
245
providerId = undefined;
246
}
247
248
if (!providerId) {
249
const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, authDetails.authorizationServerMetadata, authDetails.resourceMetadata);
250
if (!provider) {
251
return undefined;
252
}
253
providerId = provider.id;
254
}
255
256
return this._getSessionForProvider(id, server, providerId, resolvedScopes, authorizationServer, errorOnUserInteraction);
257
}
258
259
private async _getSessionForProvider(
260
serverId: number,
261
server: McpServerDefinition,
262
providerId: string,
263
scopes: string[],
264
authorizationServer?: URI,
265
errorOnUserInteraction: boolean = false
266
): Promise<string | undefined> {
267
const sessions = await this._authenticationService.getSessions(providerId, scopes, { authorizationServer }, true);
268
const accountNamePreference = this.authenticationMcpServersService.getAccountPreference(server.id, providerId);
269
let matchingAccountPreferenceSession: AuthenticationSession | undefined;
270
if (accountNamePreference) {
271
matchingAccountPreferenceSession = sessions.find(session => session.account.label === accountNamePreference);
272
}
273
const provider = this._authenticationService.getProvider(providerId);
274
let session: AuthenticationSession;
275
if (sessions.length) {
276
// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.
277
if (matchingAccountPreferenceSession && this.authenticationMCPServerAccessService.isAccessAllowed(providerId, matchingAccountPreferenceSession.account.label, server.id)) {
278
this.authenticationMCPServerUsageService.addAccountUsage(providerId, matchingAccountPreferenceSession.account.label, scopes, server.id, server.label);
279
this._serverAuthTracking.track(providerId, serverId, scopes);
280
return matchingAccountPreferenceSession.accessToken;
281
}
282
// If we only have one account for a single auth provider, lets just check if it's allowed and return it if it is.
283
if (!provider.supportsMultipleAccounts && this.authenticationMCPServerAccessService.isAccessAllowed(providerId, sessions[0].account.label, server.id)) {
284
this.authenticationMCPServerUsageService.addAccountUsage(providerId, sessions[0].account.label, scopes, server.id, server.label);
285
this._serverAuthTracking.track(providerId, serverId, scopes);
286
return sessions[0].accessToken;
287
}
288
}
289
290
if (errorOnUserInteraction) {
291
throw new UserInteractionRequiredError('authentication');
292
}
293
294
const isAllowed = await this.loginPrompt(server.label, provider.label, false);
295
if (!isAllowed) {
296
throw new Error('User did not consent to login.');
297
}
298
299
if (sessions.length) {
300
if (provider.supportsMultipleAccounts && errorOnUserInteraction) {
301
throw new UserInteractionRequiredError('authentication');
302
}
303
session = provider.supportsMultipleAccounts
304
? await this.authenticationMcpServersService.selectSession(providerId, server.id, server.label, scopes, sessions)
305
: sessions[0];
306
}
307
else {
308
if (errorOnUserInteraction) {
309
throw new UserInteractionRequiredError('authentication');
310
}
311
const accountToCreate: AuthenticationSessionAccount | undefined = matchingAccountPreferenceSession?.account;
312
do {
313
session = await this._authenticationService.createSession(
314
providerId,
315
scopes,
316
{
317
activateImmediate: true,
318
account: accountToCreate,
319
authorizationServer
320
});
321
} while (
322
accountToCreate
323
&& accountToCreate.label !== session.account.label
324
&& !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label)
325
);
326
}
327
328
this.authenticationMCPServerAccessService.updateAllowedMcpServers(providerId, session.account.label, [{ id: server.id, name: server.label, allowed: true }]);
329
this.authenticationMcpServersService.updateAccountPreference(server.id, providerId, session.account);
330
this.authenticationMCPServerUsageService.addAccountUsage(providerId, session.account.label, scopes, server.id, server.label);
331
this._serverAuthTracking.track(providerId, serverId, scopes);
332
return session.accessToken;
333
}
334
335
private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise<boolean> {
336
const result = await this.dialogService.prompt({
337
message: nls.localize('incorrectAccount', "Incorrect account detected"),
338
detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel),
339
type: Severity.Warning,
340
cancelButton: true,
341
buttons: [
342
{
343
label: nls.localize('keep', 'Keep {0}', chosenAccountLabel),
344
run: () => chosenAccountLabel
345
},
346
{
347
label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel),
348
run: () => requestedAccountLabel
349
}
350
],
351
});
352
353
if (!result.result) {
354
throw new CancellationError();
355
}
356
357
return result.result === chosenAccountLabel;
358
}
359
360
private async _onDidChangeAuthSessions(providerId: string, providerLabel: string): Promise<void> {
361
const serversUsingProvider = this._serverAuthTracking.get(providerId);
362
if (!serversUsingProvider) {
363
return;
364
}
365
366
for (const { serverId, scopes } of serversUsingProvider) {
367
const server = this._servers.get(serverId);
368
const serverDefinition = this._serverDefinitions.get(serverId);
369
370
if (!server || !serverDefinition) {
371
continue;
372
}
373
374
// Only validate servers that are running
375
const state = server.state.get();
376
if (state.state !== McpConnectionState.Kind.Running) {
377
continue;
378
}
379
380
// Validate if the session is still available
381
try {
382
await this._getSessionForProvider(serverId, serverDefinition, providerId, scopes, undefined, true);
383
} catch (e) {
384
if (UserInteractionRequiredError.is(e)) {
385
// Session is no longer valid, stop the server
386
server.pushLog(LogLevel.Warning, nls.localize('mcpAuthSessionRemoved', "Authentication session for {0} removed, stopping server", providerLabel));
387
server.stop();
388
}
389
// Ignore other errors to avoid disrupting other servers
390
}
391
}
392
}
393
394
$logMcpAuthSetup(data: IAuthMetadataSource): void {
395
type McpAuthSetupClassification = {
396
owner: 'TylerLeonhardt';
397
comment: 'Tracks how MCP OAuth authentication setup was discovered and configured';
398
resourceMetadataSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How resource metadata was discovered (header, wellKnown, or none)' };
399
serverMetadataSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How authorization server metadata was discovered (resourceMetadata, wellKnown, or default)' };
400
};
401
this._telemetryService.publicLog2<IAuthMetadataSource, McpAuthSetupClassification>('mcp/authSetup', data);
402
}
403
404
async $startMcpGateway(): Promise<{ address: URI; gatewayId: string } | undefined> {
405
const result = await this._mcpGatewayService.createGateway(this._extHostContext.extensionHostKind === ExtensionHostKind.Remote);
406
if (!result) {
407
return undefined;
408
}
409
410
if (this._store.isDisposed) {
411
result.dispose();
412
return undefined;
413
}
414
415
const gatewayId = generateUuid();
416
this._gateways.set(gatewayId, result);
417
418
return {
419
address: result.address,
420
gatewayId,
421
};
422
}
423
424
$disposeMcpGateway(gatewayId: string): void {
425
this._gateways.deleteAndDispose(gatewayId);
426
}
427
428
private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise<boolean> {
429
const message = recreatingSession
430
? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel)
431
: nls.localize('confirmLogin', "The MCP Server Definition '{0}' wants to authenticate to {1}.", mcpLabel, providerLabel);
432
433
const buttons: IPromptButton<boolean | undefined>[] = [
434
{
435
label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),
436
run() {
437
return true;
438
},
439
}
440
];
441
const { result } = await this.dialogService.prompt({
442
type: Severity.Info,
443
message,
444
buttons,
445
cancelButton: true,
446
});
447
448
return result ?? false;
449
}
450
451
override dispose(): void {
452
for (const server of this._servers.values()) {
453
server.extHostDispose();
454
}
455
this._servers.clear();
456
this._serverDefinitions.clear();
457
this._serverAuthTracking.clear();
458
super.dispose();
459
}
460
}
461
462
463
class ExtHostMcpServerLaunch extends Disposable implements IMcpMessageTransport {
464
public readonly state = observableValue<McpConnectionState>('mcpServerState', { state: McpConnectionState.Kind.Starting });
465
466
private readonly _onDidLog = this._register(new Emitter<{ level: LogLevel; message: string }>());
467
public readonly onDidLog = this._onDidLog.event;
468
469
private readonly _onDidReceiveMessage = this._register(new Emitter<MCP.JSONRPCMessage>());
470
public readonly onDidReceiveMessage = this._onDidReceiveMessage.event;
471
472
pushLog(level: LogLevel, message: string): void {
473
this._onDidLog.fire({ message, level });
474
}
475
476
pushMessage(message: string): void {
477
let parsed: MCP.JSONRPCMessage | undefined;
478
try {
479
parsed = JSON.parse(message);
480
} catch (e) {
481
this.pushLog(LogLevel.Warning, `Failed to parse message: ${JSON.stringify(message)}`);
482
}
483
484
if (parsed) {
485
if (Array.isArray(parsed)) { // streamable HTTP supports batching
486
parsed.forEach(p => this._onDidReceiveMessage.fire(p));
487
} else {
488
this._onDidReceiveMessage.fire(parsed);
489
}
490
}
491
}
492
493
constructor(
494
extHostKind: ExtensionHostKind,
495
public readonly stop: () => void,
496
public readonly send: (message: MCP.JSONRPCMessage) => void,
497
) {
498
super();
499
500
this._register(disposableTimeout(() => {
501
this.pushLog(LogLevel.Info, `Starting server from ${extensionHostKindToString(extHostKind)} extension host`);
502
}));
503
}
504
505
public extHostDispose() {
506
if (McpConnectionState.isRunning(this.state.get())) {
507
this.pushLog(LogLevel.Warning, 'Extension host shut down, server will stop.');
508
this.state.set({ state: McpConnectionState.Kind.Stopped }, undefined);
509
}
510
this.dispose();
511
}
512
513
public override dispose(): void {
514
if (McpConnectionState.isRunning(this.state.get())) {
515
this.stop();
516
}
517
518
super.dispose();
519
}
520
}
521
522
/**
523
* Tracks which MCP servers are using which authentication providers.
524
* Organized by provider ID for efficient lookup when auth sessions change.
525
*/
526
class McpServerAuthTracker {
527
// Provider ID -> Array of serverId and scopes used
528
private readonly _tracking = new Map<string, Array<{ serverId: number; scopes: string[] }>>();
529
530
/**
531
* Track authentication for a server with a specific provider.
532
* Replaces any existing tracking for this server/provider combination.
533
*/
534
track(providerId: string, serverId: number, scopes: string[]): void {
535
const servers = this._tracking.get(providerId) || [];
536
const filtered = servers.filter(s => s.serverId !== serverId);
537
filtered.push({ serverId, scopes });
538
this._tracking.set(providerId, filtered);
539
}
540
541
/**
542
* Remove all authentication tracking for a server across all providers.
543
*/
544
untrack(serverId: number): void {
545
for (const [providerId, servers] of this._tracking.entries()) {
546
const filtered = servers.filter(s => s.serverId !== serverId);
547
if (filtered.length === 0) {
548
this._tracking.delete(providerId);
549
} else {
550
this._tracking.set(providerId, filtered);
551
}
552
}
553
}
554
555
/**
556
* Get all servers using a specific authentication provider.
557
*/
558
get(providerId: string): ReadonlyArray<{ serverId: number; scopes: string[] }> | undefined {
559
return this._tracking.get(providerId);
560
}
561
562
/**
563
* Clear all tracking data.
564
*/
565
clear(): void {
566
this._tracking.clear();
567
}
568
}
569
570