Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.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 { Sequencer } from '../../../../base/common/async.js';6import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';7import { Lazy } from '../../../../base/common/lazy.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { isEmptyObject } from '../../../../base/common/types.js';10import { ILogService } from '../../../../platform/log/common/log.js';11import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';12import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';13import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js';1415const MCP_ENCRYPTION_KEY_NAME = 'mcpEncryptionKey';16const MCP_ENCRYPTION_KEY_ALGORITHM = 'AES-GCM';17const MCP_ENCRYPTION_KEY_LEN = 256;18const MCP_ENCRYPTION_IV_LENGTH = 12; // 96 bits19const MCP_DATA_STORED_VERSION = 1;20const MCP_DATA_STORED_KEY = 'mcpInputs';2122interface IStoredData {23version: number;24values: Record<string, IResolvedValue>;25secrets?: { value: string; iv: string }; // base64, encrypted26}2728interface IHydratedData extends IStoredData {29unsealedSecrets?: Record<string, IResolvedValue>;30}3132export class McpRegistryInputStorage extends Disposable {33private static secretSequencer = new Sequencer();34private readonly _secretsSealerSequencer = new Sequencer();3536private readonly _getEncryptionKey = new Lazy(() => {37return McpRegistryInputStorage.secretSequencer.queue(async () => {38const existing = await this._secretStorageService.get(MCP_ENCRYPTION_KEY_NAME);39if (existing) {40try {41const parsed: JsonWebKey = JSON.parse(existing);42return await crypto.subtle.importKey('jwk', parsed, MCP_ENCRYPTION_KEY_ALGORITHM, false, ['encrypt', 'decrypt']);43} catch {44// fall through45}46}4748const key = await crypto.subtle.generateKey(49{ name: MCP_ENCRYPTION_KEY_ALGORITHM, length: MCP_ENCRYPTION_KEY_LEN },50true,51['encrypt', 'decrypt'],52);5354const exported = await crypto.subtle.exportKey('jwk', key);55await this._secretStorageService.set(MCP_ENCRYPTION_KEY_NAME, JSON.stringify(exported));56return key;57});58});5960private _didChange = false;6162private _record = new Lazy<IHydratedData>(() => {63const stored = this._storageService.getObject<IStoredData>(MCP_DATA_STORED_KEY, this._scope);64return stored?.version === MCP_DATA_STORED_VERSION ? { ...stored } : { version: MCP_DATA_STORED_VERSION, values: {} };65});666768constructor(69private readonly _scope: StorageScope,70_target: StorageTarget,71@IStorageService private readonly _storageService: IStorageService,72@ISecretStorageService private readonly _secretStorageService: ISecretStorageService,73@ILogService private readonly _logService: ILogService,74) {75super();7677this._register(_storageService.onWillSaveState(() => {78if (this._didChange) {79this._storageService.store(MCP_DATA_STORED_KEY, {80version: MCP_DATA_STORED_VERSION,81values: this._record.value.values,82secrets: this._record.value.secrets,83} satisfies IStoredData, this._scope, _target);84this._didChange = false;85}86}));87}8889/** Deletes all collection data from storage. */90public clearAll() {91this._record.value.values = {};92this._record.value.secrets = undefined;93this._record.value.unsealedSecrets = undefined;94this._didChange = true;95}9697/** Delete a single collection data from the storage. */98public async clear(inputKey: string) {99const secrets = await this._unsealSecrets();100delete this._record.value.values[inputKey];101this._didChange = true;102103if (secrets.hasOwnProperty(inputKey)) {104delete secrets[inputKey];105await this._sealSecrets();106}107}108109/** Gets a mapping of saved input data. */110public async getMap() {111const secrets = await this._unsealSecrets();112return { ...this._record.value.values, ...secrets };113}114115/** Updates the input data mapping. */116public async setPlainText(values: Record<string, IResolvedValue>) {117Object.assign(this._record.value.values, values);118this._didChange = true;119}120121/** Updates the input secrets mapping. */122public async setSecrets(values: Record<string, IResolvedValue>) {123const unsealed = await this._unsealSecrets();124Object.assign(unsealed, values);125await this._sealSecrets();126}127128private async _sealSecrets() {129const key = await this._getEncryptionKey.value;130return this._secretsSealerSequencer.queue(async () => {131if (!this._record.value.unsealedSecrets || isEmptyObject(this._record.value.unsealedSecrets)) {132this._record.value.secrets = undefined;133return;134}135136const toSeal = JSON.stringify(this._record.value.unsealedSecrets);137const iv = crypto.getRandomValues(new Uint8Array(MCP_ENCRYPTION_IV_LENGTH));138const encrypted = await crypto.subtle.encrypt(139{ name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer },140key,141new TextEncoder().encode(toSeal).buffer as ArrayBuffer,142);143144const enc = encodeBase64(VSBuffer.wrap(new Uint8Array(encrypted)));145this._record.value.secrets = { iv: encodeBase64(VSBuffer.wrap(iv)), value: enc };146this._didChange = true;147});148}149150private async _unsealSecrets(): Promise<Record<string, IResolvedValue>> {151if (!this._record.value.secrets) {152return this._record.value.unsealedSecrets ??= {};153}154155if (this._record.value.unsealedSecrets) {156return this._record.value.unsealedSecrets;157}158159try {160const key = await this._getEncryptionKey.value;161const iv = decodeBase64(this._record.value.secrets.iv);162const encrypted = decodeBase64(this._record.value.secrets.value);163164const decrypted = await crypto.subtle.decrypt(165{ name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer as Uint8Array<ArrayBuffer> },166key,167encrypted.buffer as Uint8Array<ArrayBuffer>,168);169170const unsealedSecrets = JSON.parse(new TextDecoder().decode(decrypted));171this._record.value.unsealedSecrets = unsealedSecrets;172return unsealedSecrets;173} catch (e) {174this._logService.warn('Error unsealing MCP secrets', e);175this._record.value.secrets = undefined;176}177178return {};179}180}181182183