Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.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 { Sequencer } from '../../../../base/common/async.js';
7
import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
8
import { Lazy } from '../../../../base/common/lazy.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { isEmptyObject } from '../../../../base/common/types.js';
11
import { ILogService } from '../../../../platform/log/common/log.js';
12
import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
13
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
14
import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js';
15
16
const MCP_ENCRYPTION_KEY_NAME = 'mcpEncryptionKey';
17
const MCP_ENCRYPTION_KEY_ALGORITHM = 'AES-GCM';
18
const MCP_ENCRYPTION_KEY_LEN = 256;
19
const MCP_ENCRYPTION_IV_LENGTH = 12; // 96 bits
20
const MCP_DATA_STORED_VERSION = 1;
21
const MCP_DATA_STORED_KEY = 'mcpInputs';
22
23
interface IStoredData {
24
version: number;
25
values: Record<string, IResolvedValue>;
26
secrets?: { value: string; iv: string }; // base64, encrypted
27
}
28
29
interface IHydratedData extends IStoredData {
30
unsealedSecrets?: Record<string, IResolvedValue>;
31
}
32
33
export class McpRegistryInputStorage extends Disposable {
34
private static secretSequencer = new Sequencer();
35
private readonly _secretsSealerSequencer = new Sequencer();
36
37
private readonly _getEncryptionKey = new Lazy(() => {
38
return McpRegistryInputStorage.secretSequencer.queue(async () => {
39
const existing = await this._secretStorageService.get(MCP_ENCRYPTION_KEY_NAME);
40
if (existing) {
41
try {
42
const parsed: JsonWebKey = JSON.parse(existing);
43
return await crypto.subtle.importKey('jwk', parsed, MCP_ENCRYPTION_KEY_ALGORITHM, false, ['encrypt', 'decrypt']);
44
} catch {
45
// fall through
46
}
47
}
48
49
const key = await crypto.subtle.generateKey(
50
{ name: MCP_ENCRYPTION_KEY_ALGORITHM, length: MCP_ENCRYPTION_KEY_LEN },
51
true,
52
['encrypt', 'decrypt'],
53
);
54
55
const exported = await crypto.subtle.exportKey('jwk', key);
56
await this._secretStorageService.set(MCP_ENCRYPTION_KEY_NAME, JSON.stringify(exported));
57
return key;
58
});
59
});
60
61
private _didChange = false;
62
63
private _record = new Lazy<IHydratedData>(() => {
64
const stored = this._storageService.getObject<IStoredData>(MCP_DATA_STORED_KEY, this._scope);
65
return stored?.version === MCP_DATA_STORED_VERSION ? { ...stored } : { version: MCP_DATA_STORED_VERSION, values: {} };
66
});
67
68
69
constructor(
70
private readonly _scope: StorageScope,
71
_target: StorageTarget,
72
@IStorageService private readonly _storageService: IStorageService,
73
@ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
74
@ILogService private readonly _logService: ILogService,
75
) {
76
super();
77
78
this._register(_storageService.onWillSaveState(() => {
79
if (this._didChange) {
80
this._storageService.store(MCP_DATA_STORED_KEY, {
81
version: MCP_DATA_STORED_VERSION,
82
values: this._record.value.values,
83
secrets: this._record.value.secrets,
84
} satisfies IStoredData, this._scope, _target);
85
this._didChange = false;
86
}
87
}));
88
}
89
90
/** Deletes all collection data from storage. */
91
public clearAll() {
92
this._record.value.values = {};
93
this._record.value.secrets = undefined;
94
this._record.value.unsealedSecrets = undefined;
95
this._didChange = true;
96
}
97
98
/** Delete a single collection data from the storage. */
99
public async clear(inputKey: string) {
100
const secrets = await this._unsealSecrets();
101
delete this._record.value.values[inputKey];
102
this._didChange = true;
103
104
if (secrets.hasOwnProperty(inputKey)) {
105
delete secrets[inputKey];
106
await this._sealSecrets();
107
}
108
}
109
110
/** Gets a mapping of saved input data. */
111
public async getMap() {
112
const secrets = await this._unsealSecrets();
113
return { ...this._record.value.values, ...secrets };
114
}
115
116
/** Updates the input data mapping. */
117
public async setPlainText(values: Record<string, IResolvedValue>) {
118
Object.assign(this._record.value.values, values);
119
this._didChange = true;
120
}
121
122
/** Updates the input secrets mapping. */
123
public async setSecrets(values: Record<string, IResolvedValue>) {
124
const unsealed = await this._unsealSecrets();
125
Object.assign(unsealed, values);
126
await this._sealSecrets();
127
}
128
129
private async _sealSecrets() {
130
const key = await this._getEncryptionKey.value;
131
return this._secretsSealerSequencer.queue(async () => {
132
if (!this._record.value.unsealedSecrets || isEmptyObject(this._record.value.unsealedSecrets)) {
133
this._record.value.secrets = undefined;
134
return;
135
}
136
137
const toSeal = JSON.stringify(this._record.value.unsealedSecrets);
138
const iv = crypto.getRandomValues(new Uint8Array(MCP_ENCRYPTION_IV_LENGTH));
139
const encrypted = await crypto.subtle.encrypt(
140
{ name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer },
141
key,
142
new TextEncoder().encode(toSeal).buffer as ArrayBuffer,
143
);
144
145
const enc = encodeBase64(VSBuffer.wrap(new Uint8Array(encrypted)));
146
this._record.value.secrets = { iv: encodeBase64(VSBuffer.wrap(iv)), value: enc };
147
this._didChange = true;
148
});
149
}
150
151
private async _unsealSecrets(): Promise<Record<string, IResolvedValue>> {
152
if (!this._record.value.secrets) {
153
return this._record.value.unsealedSecrets ??= {};
154
}
155
156
if (this._record.value.unsealedSecrets) {
157
return this._record.value.unsealedSecrets;
158
}
159
160
try {
161
const key = await this._getEncryptionKey.value;
162
const iv = decodeBase64(this._record.value.secrets.iv);
163
const encrypted = decodeBase64(this._record.value.secrets.value);
164
165
const decrypted = await crypto.subtle.decrypt(
166
{ name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer as Uint8Array<ArrayBuffer> },
167
key,
168
encrypted.buffer as Uint8Array<ArrayBuffer>,
169
);
170
171
const unsealedSecrets = JSON.parse(new TextDecoder().decode(decrypted));
172
this._record.value.unsealedSecrets = unsealedSecrets;
173
return unsealedSecrets;
174
} catch (e) {
175
this._logService.warn('Error unsealing MCP secrets', e);
176
this._record.value.secrets = undefined;
177
}
178
179
return {};
180
}
181
}
182
183