Path: blob/main/src/vs/workbench/services/authentication/browser/authenticationMcpAccessService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import { Disposable } from '../../../../base/common/lifecycle.js';7import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';8import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';9import { IProductService } from '../../../../platform/product/common/productService.js';10import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';1112export interface AllowedMcpServer {13id: string;14name: string;15/**16* If true or undefined, the extension is allowed to use the account17* If false, the extension is not allowed to use the account18* TODO: undefined shouldn't be a valid value, but it is for now19*/20allowed?: boolean;21lastUsed?: number;22// If true, this comes from the product.json23trusted?: boolean;24}2526export const IAuthenticationMcpAccessService = createDecorator<IAuthenticationMcpAccessService>('IAuthenticationMcpAccessService');27export interface IAuthenticationMcpAccessService {28readonly _serviceBrand: undefined;2930readonly onDidChangeMcpSessionAccess: Event<{ providerId: string; accountName: string }>;3132/**33* Check MCP server access to an account34* @param providerId The id of the authentication provider35* @param accountName The account name that access is checked for36* @param mcpServerId The id of the MCP server requesting access37* @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined38* if they haven't made a choice yet39*/40isAccessAllowed(providerId: string, accountName: string, mcpServerId: string): boolean | undefined;41readAllowedMcpServers(providerId: string, accountName: string): AllowedMcpServer[];42updateAllowedMcpServers(providerId: string, accountName: string, mcpServers: AllowedMcpServer[]): void;43removeAllowedMcpServers(providerId: string, accountName: string): void;44}4546// TODO@TylerLeonhardt: Should this class only keep track of allowed things and throw away disallowed ones?47export class AuthenticationMcpAccessService extends Disposable implements IAuthenticationMcpAccessService {48_serviceBrand: undefined;4950private _onDidChangeMcpSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>());51readonly onDidChangeMcpSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeMcpSessionAccess.event;5253constructor(54@IStorageService private readonly _storageService: IStorageService,55@IProductService private readonly _productService: IProductService56) {57super();58}5960isAccessAllowed(providerId: string, accountName: string, mcpServerId: string): boolean | undefined {61const trustedMCPServerAuthAccess = this._productService.trustedMcpAuthAccess;62if (Array.isArray(trustedMCPServerAuthAccess)) {63if (trustedMCPServerAuthAccess.includes(mcpServerId)) {64return true;65}66} else if (trustedMCPServerAuthAccess?.[providerId]?.includes(mcpServerId)) {67return true;68}6970const allowList = this.readAllowedMcpServers(providerId, accountName);71const mcpServerData = allowList.find(mcpServer => mcpServer.id === mcpServerId);72if (!mcpServerData) {73return undefined;74}75// This property didn't exist on this data previously, inclusion in the list at all indicates allowance76return mcpServerData.allowed !== undefined77? mcpServerData.allowed78: true;79}8081readAllowedMcpServers(providerId: string, accountName: string): AllowedMcpServer[] {82let trustedMCPServers: AllowedMcpServer[] = [];83try {84const trustedMCPServerSrc = this._storageService.get(`mcpserver-${providerId}-${accountName}`, StorageScope.APPLICATION);85if (trustedMCPServerSrc) {86trustedMCPServers = JSON.parse(trustedMCPServerSrc);87}88} catch (err) { }8990// Add trusted MCP servers from product.json if they're not already in the list91const trustedMcpServerAuthAccess = this._productService.trustedMcpAuthAccess;92const trustedMcpServerIds =93// Case 1: trustedMcpServerAuthAccess is an array94Array.isArray(trustedMcpServerAuthAccess)95? trustedMcpServerAuthAccess96// Case 2: trustedMcpServerAuthAccess is an object97: typeof trustedMcpServerAuthAccess === 'object'98? trustedMcpServerAuthAccess[providerId] ?? []99: [];100101for (const mcpServerId of trustedMcpServerIds) {102const existingServer = trustedMCPServers.find(server => server.id === mcpServerId);103if (!existingServer) {104// Add new trusted server (name will be set by caller if they have server info)105trustedMCPServers.push({106id: mcpServerId,107name: mcpServerId, // Default to ID, caller can update with proper name108allowed: true,109trusted: true110});111} else {112// Update existing server to be trusted113existingServer.allowed = true;114existingServer.trusted = true;115}116}117118return trustedMCPServers;119}120121updateAllowedMcpServers(providerId: string, accountName: string, mcpServers: AllowedMcpServer[]): void {122const allowList = this.readAllowedMcpServers(providerId, accountName);123for (const mcpServer of mcpServers) {124const index = allowList.findIndex(e => e.id === mcpServer.id);125if (index === -1) {126allowList.push(mcpServer);127} else {128allowList[index].allowed = mcpServer.allowed;129// Update name if provided and not already set to a proper name130if (mcpServer.name && mcpServer.name !== mcpServer.id && allowList[index].name !== mcpServer.name) {131allowList[index].name = mcpServer.name;132}133}134}135136// Filter out trusted servers before storing - they should only come from product.json, not user storage137const userManagedServers = allowList.filter(server => !server.trusted);138this._storageService.store(`mcpserver-${providerId}-${accountName}`, JSON.stringify(userManagedServers), StorageScope.APPLICATION, StorageTarget.USER);139this._onDidChangeMcpSessionAccess.fire({ providerId, accountName });140}141142removeAllowedMcpServers(providerId: string, accountName: string): void {143this._storageService.remove(`mcpserver-${providerId}-${accountName}`, StorageScope.APPLICATION);144this._onDidChangeMcpSessionAccess.fire({ providerId, accountName });145}146}147148registerSingleton(IAuthenticationMcpAccessService, AuthenticationMcpAccessService, InstantiationType.Delayed);149150151