Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts
13401 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 { VSBuffer } from '../../../../base/common/buffer.js';
7
import { Disposable } from '../../../../base/common/lifecycle.js';
8
import { FileAccess } from '../../../../base/common/network.js';
9
import { dirname, posix, win32 } from '../../../../base/common/path.js';
10
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { generateUuid } from '../../../../base/common/uuid.js';
13
import { localize } from '../../../../nls.js';
14
import { ConfigurationTarget, ConfigurationTargetToString } from '../../../../platform/configuration/common/configuration.js';
15
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
16
import { IFileService } from '../../../../platform/files/common/files.js';
17
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
18
import { ILogService } from '../../../../platform/log/common/log.js';
19
import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platform/mcp/common/mcpResourceScannerService.js';
20
import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js';
21
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
22
import { IMcpSandboxConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
23
import { IMcpPotentialSandboxBlock, McpServerDefinition, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from './mcpTypes.js';
24
25
26
export const IMcpSandboxService = createDecorator<IMcpSandboxService>('mcpSandboxService');
27
28
export interface IMcpSandboxService {
29
readonly _serviceBrand: undefined;
30
launchInSandboxIfEnabled(serverDef: McpServerDefinition, launch: McpServerLaunch, remoteAuthority: string | undefined, configTarget: ConfigurationTarget): Promise<McpServerLaunch>;
31
isEnabled(serverDef: McpServerDefinition, serverLabel?: string): Promise<boolean>;
32
getSandboxConfigSuggestionMessage(serverLabel: string, potentialBlocks: readonly IMcpPotentialSandboxBlock[], existingSandboxConfig?: IMcpSandboxConfiguration): SandboxConfigSuggestionResult | undefined;
33
applySandboxConfigSuggestion(serverDef: McpServerDefinition, mcpResource: URI, configTarget: ConfigurationTarget, potentialBlocks: readonly IMcpPotentialSandboxBlock[], suggestedSandboxConfig?: IMcpSandboxConfiguration): Promise<boolean>;
34
}
35
36
type SandboxConfigSuggestions = {
37
allowWrite: readonly string[];
38
allowedDomains: readonly string[];
39
};
40
41
type SandboxConfigSuggestionResult = {
42
message: string;
43
sandboxConfig: IMcpSandboxConfiguration;
44
};
45
46
type SandboxLaunchDetails = {
47
execPath: string | undefined;
48
srtPath: string | undefined;
49
rgPath: string | undefined;
50
sandboxConfigPath: string | undefined;
51
tempDir: URI | undefined;
52
};
53
54
export class McpSandboxService extends Disposable implements IMcpSandboxService {
55
readonly _serviceBrand: undefined;
56
57
private _sandboxSettingsId: string | undefined;
58
private _remoteEnvDetailsPromise: Promise<IRemoteAgentEnvironment | null>;
59
private readonly _defaultAllowedDomains: readonly string[] = ['registry.npmjs.org']; // Default allowed domains that are commonly needed for MCP servers, even if the user doesn't specify them in their sandbox config
60
private _defaultAllowWritePaths: string[] = ['~/.npm'];
61
private _sandboxConfigPerConfigurationTarget: Map<string, string> = new Map();
62
63
constructor(
64
@IFileService private readonly _fileService: IFileService,
65
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
66
@ILogService private readonly _logService: ILogService,
67
@IMcpResourceScannerService private readonly _mcpResourceScannerService: IMcpResourceScannerService,
68
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
69
) {
70
super();
71
this._sandboxSettingsId = generateUuid();
72
this._remoteEnvDetailsPromise = this._remoteAgentService.getEnvironment();
73
74
}
75
76
public async isEnabled(serverDef: McpServerDefinition, remoteAuthority?: string): Promise<boolean> {
77
const os = await this._getOperatingSystem(remoteAuthority);
78
if (os === OperatingSystem.Windows) {
79
return false;
80
}
81
return !!serverDef.sandboxEnabled;
82
}
83
84
public async launchInSandboxIfEnabled(serverDef: McpServerDefinition, launch: McpServerLaunch, remoteAuthority: string | undefined, configTarget: ConfigurationTarget): Promise<McpServerLaunch> {
85
if (launch.type !== McpServerTransportType.Stdio) {
86
return launch;
87
}
88
if (await this.isEnabled(serverDef, remoteAuthority)) {
89
this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`);
90
const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, launch.sandbox, launch.cwd);
91
const quotedCommand = this._quoteShellArgument(launch.command);
92
const quotedArgs = launch.args.map(arg => this._quoteShellArgument(arg));
93
const sandboxArgs = this._getSandboxCommandArgs(quotedCommand, quotedArgs, launchDetails.sandboxConfigPath);
94
const sandboxEnv = await this._getSandboxEnvVariables(launch.env, launchDetails.tempDir, launchDetails.rgPath, remoteAuthority);
95
if (launchDetails.srtPath) {
96
if (launchDetails.execPath) {
97
return {
98
...launch,
99
command: launchDetails.execPath,
100
args: [launchDetails.srtPath, ...sandboxArgs],
101
env: sandboxEnv,
102
type: McpServerTransportType.Stdio,
103
};
104
} else {
105
return {
106
...launch,
107
command: launchDetails.srtPath,
108
args: sandboxArgs,
109
env: sandboxEnv,
110
type: McpServerTransportType.Stdio,
111
};
112
}
113
}
114
if (!launchDetails.execPath) {
115
this._logService.warn('McpSandboxService: execPath is unavailable, launching without sandbox runtime wrapper');
116
}
117
this._logService.debug(`McpSandboxService: launch details for server ${serverDef.label} - command: ${launch.command}, args: ${launch.args.join(' ')}`);
118
}
119
return launch;
120
}
121
122
public getSandboxConfigSuggestionMessage(serverLabel: string, potentialBlocks: readonly IMcpPotentialSandboxBlock[], existingSandboxConfig?: IMcpSandboxConfiguration): SandboxConfigSuggestionResult | undefined {
123
const suggestions = this._getSandboxConfigSuggestions(potentialBlocks, existingSandboxConfig);
124
if (!suggestions) {
125
return undefined;
126
}
127
128
const allowWriteList = suggestions.allowWrite;
129
const allowedDomainsList = suggestions.allowedDomains;
130
const suggestionLines: string[] = [];
131
132
if (allowedDomainsList.length) {
133
const shown = allowedDomainsList.map(domain => `"${domain}"`).join(', ');
134
suggestionLines.push(localize('mcpSandboxSuggestion.allowedDomains', "Add to `sandbox.network.allowedDomains`: {0}", shown));
135
}
136
137
if (allowWriteList.length) {
138
const shown = allowWriteList.map(path => `"${path}"`).join(', ');
139
suggestionLines.push(localize('mcpSandboxSuggestion.allowWrite', "Add to `sandbox.filesystem.allowWrite`: {0}", shown));
140
}
141
142
const sandboxConfig: IMcpSandboxConfiguration = {};
143
if (allowedDomainsList.length) {
144
sandboxConfig.network = { allowedDomains: [...allowedDomainsList] };
145
}
146
if (allowWriteList.length) {
147
sandboxConfig.filesystem = { allowWrite: [...allowWriteList] };
148
}
149
150
return {
151
message: localize(
152
'mcpSandboxSuggestion.message',
153
"The MCP server {0} reported potential sandbox blocks. VS Code found possible sandbox configuration updates:\n{1}",
154
serverLabel,
155
suggestionLines.join('\n')
156
),
157
sandboxConfig,
158
};
159
}
160
161
public async applySandboxConfigSuggestion(serverDef: McpServerDefinition, mcpResource: URI, configTarget: ConfigurationTarget, potentialBlocks: readonly IMcpPotentialSandboxBlock[], suggestedSandboxConfig?: IMcpSandboxConfiguration): Promise<boolean> {
162
const scanTarget = this._toMcpResourceTarget(configTarget);
163
let didChange = false;
164
165
await this._mcpResourceScannerService.updateSandboxConfig(data => {
166
const existingSandbox = data.sandbox;
167
const suggestedAllowedDomains = suggestedSandboxConfig?.network?.allowedDomains ?? [];
168
const suggestedAllowWrite = suggestedSandboxConfig?.filesystem?.allowWrite ?? [];
169
170
const currentAllowedDomains = new Set(existingSandbox?.network?.allowedDomains ?? []);
171
for (const domain of suggestedAllowedDomains) {
172
if (domain && !currentAllowedDomains.has(domain)) {
173
currentAllowedDomains.add(domain);
174
}
175
}
176
177
const currentAllowWrite = new Set(existingSandbox?.filesystem?.allowWrite ?? []);
178
for (const path of suggestedAllowWrite) {
179
if (path && !currentAllowWrite.has(path)) {
180
currentAllowWrite.add(path);
181
}
182
}
183
184
if (suggestedAllowedDomains.length === 0 && suggestedAllowWrite.length === 0) {
185
return data;
186
}
187
188
didChange = true;
189
const nextSandboxConfig: IMcpSandboxConfiguration = {};
190
if (currentAllowedDomains.size > 0) {
191
nextSandboxConfig.network = {
192
...existingSandbox?.network,
193
allowedDomains: [...currentAllowedDomains]
194
};
195
}
196
if (currentAllowWrite.size > 0) {
197
nextSandboxConfig.filesystem = {
198
...existingSandbox?.filesystem,
199
allowWrite: [...currentAllowWrite],
200
};
201
}
202
return {
203
...data,
204
sandbox: nextSandboxConfig,
205
};
206
}, mcpResource, scanTarget);
207
208
return didChange;
209
}
210
211
private _getSandboxConfigSuggestions(potentialBlocks: readonly IMcpPotentialSandboxBlock[], existingSandboxConfig?: IMcpSandboxConfiguration): SandboxConfigSuggestions | undefined {
212
if (!potentialBlocks.length) {
213
return undefined;
214
}
215
216
const allowWrite = new Set<string>();
217
const allowedDomains = new Set<string>();
218
const existingAllowWrite = new Set(existingSandboxConfig?.filesystem?.allowWrite ?? []);
219
const existingAllowedDomains = new Set(existingSandboxConfig?.network?.allowedDomains ?? []);
220
221
for (const block of potentialBlocks) {
222
if (block.kind === 'network' && block.host && !existingAllowedDomains.has(block.host)) {
223
allowedDomains.add(block.host);
224
}
225
226
if (block.kind === 'filesystem' && block.path && !existingAllowWrite.has(block.path)) {
227
allowWrite.add(block.path);
228
}
229
}
230
231
if (!allowWrite.size && !allowedDomains.size) {
232
return undefined;
233
}
234
235
return {
236
allowWrite: [...allowWrite],
237
allowedDomains: [...allowedDomains],
238
};
239
}
240
241
private _toMcpResourceTarget(configTarget: ConfigurationTarget): McpResourceTarget {
242
switch (configTarget) {
243
case ConfigurationTarget.USER:
244
case ConfigurationTarget.USER_LOCAL:
245
case ConfigurationTarget.USER_REMOTE:
246
return ConfigurationTarget.USER;
247
case ConfigurationTarget.WORKSPACE:
248
return ConfigurationTarget.WORKSPACE;
249
case ConfigurationTarget.WORKSPACE_FOLDER:
250
return ConfigurationTarget.WORKSPACE_FOLDER;
251
default:
252
return ConfigurationTarget.USER;
253
}
254
}
255
256
private async _resolveSandboxLaunchDetails(configTarget: ConfigurationTarget, remoteAuthority?: string, sandboxConfig?: IMcpSandboxConfiguration, launchCwd?: string): Promise<SandboxLaunchDetails> {
257
const os = await this._getOperatingSystem(remoteAuthority);
258
if (os === OperatingSystem.Windows) {
259
return { execPath: undefined, srtPath: undefined, rgPath: undefined, sandboxConfigPath: undefined, tempDir: undefined };
260
}
261
262
const appRoot = await this._getAppRoot(remoteAuthority);
263
const execPath = await this._getExecPath(os, appRoot, remoteAuthority);
264
const tempDir = await this._getTempDir(remoteAuthority);
265
const srtPath = this._pathJoin(os, appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js');
266
const rgPath = this._pathJoin(os, appRoot, 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg');
267
const sandboxConfigPath = tempDir ? await this._updateSandboxConfig(tempDir, configTarget, sandboxConfig, launchCwd) : undefined;
268
this._logService.debug(`McpSandboxService: Updated sandbox config path: ${sandboxConfigPath}`);
269
return { execPath, srtPath, rgPath, sandboxConfigPath, tempDir };
270
}
271
272
private async _getExecPath(os: OperatingSystem, appRoot: string, remoteAuthority?: string): Promise<string | undefined> {
273
if (remoteAuthority) {
274
return this._pathJoin(os, appRoot, 'node');
275
}
276
return undefined; // Use Electron executable as the default exec path for local development, which will run the sandbox runtime wrapper with Electron in node mode. For remote, we need to specify the node executable to ensure it runs with Node.js.
277
}
278
279
private async _getSandboxEnvVariables(baseEnv: McpServerTransportStdio['env'], tempDir: URI | undefined, rgPath: string | undefined, remoteAuthority?: string): Promise<McpServerTransportStdio['env']> {
280
let env: McpServerTransportStdio['env'] = { ...baseEnv };
281
if (tempDir) {
282
env = { ...env, TMPDIR: tempDir.path, SRT_DEBUG: 'true', NODE_USE_ENV_PROXY: '1' };
283
}
284
if (rgPath) {
285
env = { ...env, PATH: env['PATH'] ? `${env['PATH']}${await this._getPathDelimiter(remoteAuthority)}${dirname(rgPath)}` : dirname(rgPath) };
286
}
287
if (!remoteAuthority) {
288
// Add any remote-specific environment variables here
289
env = { ...env, ELECTRON_RUN_AS_NODE: '1' };
290
}
291
// Ensure VSCODE_INSPECTOR_OPTIONS is not inherited by the sandboxed process, as it can cause issues with sandboxing.
292
env['VSCODE_INSPECTOR_OPTIONS'] = null;
293
return env;
294
}
295
296
private _getSandboxCommandArgs(command: string, args: readonly string[], sandboxConfigPath: string | undefined): string[] {
297
const result: string[] = [];
298
if (sandboxConfigPath) {
299
result.push('--settings', sandboxConfigPath);
300
result.push('--');
301
}
302
result.push(command, ...args);
303
return result;
304
}
305
306
private async _getRemoteEnv(remoteAuthority?: string): Promise<IRemoteAgentEnvironment | null> {
307
if (!remoteAuthority) {
308
return null;
309
}
310
return this._remoteEnvDetailsPromise;
311
}
312
313
private async _getOperatingSystem(remoteAuthority?: string): Promise<OperatingSystem> {
314
const remoteEnv = await this._getRemoteEnv(remoteAuthority);
315
if (remoteEnv) {
316
return remoteEnv.os;
317
}
318
return OS;
319
}
320
321
private async _getAppRoot(remoteAuthority?: string): Promise<string> {
322
const remoteEnv = await this._getRemoteEnv(remoteAuthority);
323
if (remoteEnv) {
324
return remoteEnv.appRoot.path;
325
}
326
return dirname(FileAccess.asFileUri('').path);
327
}
328
329
private async _getTempDir(remoteAuthority?: string): Promise<URI | undefined> {
330
const remoteEnv = await this._getRemoteEnv(remoteAuthority);
331
if (remoteEnv) {
332
return remoteEnv.tmpDir;
333
}
334
const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI };
335
const tempDir = environmentService.tmpDir;
336
if (!tempDir) {
337
this._logService.warn('McpSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment');
338
}
339
return tempDir;
340
}
341
342
private async _updateSandboxConfig(tempDir: URI, configTarget: ConfigurationTarget, sandboxConfig?: IMcpSandboxConfiguration, launchCwd?: string): Promise<string> {
343
const normalizedSandboxConfig = this._withDefaultSandboxConfig(sandboxConfig, launchCwd);
344
let configFileUri: URI;
345
const configTargetKey = ConfigurationTargetToString(configTarget);
346
if (this._sandboxConfigPerConfigurationTarget.has(configTargetKey)) {
347
configFileUri = URI.parse(this._sandboxConfigPerConfigurationTarget.get(configTargetKey)!);
348
} else {
349
configFileUri = URI.joinPath(tempDir, `vscode-${configTargetKey}-mcp-sandbox-settings-${this._sandboxSettingsId}.json`);
350
this._sandboxConfigPerConfigurationTarget.set(configTargetKey, configFileUri.toString());
351
}
352
await this._fileService.createFile(configFileUri, VSBuffer.fromString(JSON.stringify(normalizedSandboxConfig, null, '\t')), { overwrite: true });
353
return configFileUri.path;
354
}
355
356
// this method merges the default allowWrite paths and allowedDomains with the ones provided in the sandbox config, to ensure that the default necessary paths and domains are always included in the sandbox config used for launching,
357
// even if they are not explicitly specified in the config provided by the user or the MCP server config.
358
private _withDefaultSandboxConfig(sandboxConfig?: IMcpSandboxConfiguration, launchCwd?: string): IMcpSandboxConfiguration {
359
const mergedAllowWrite = new Set(sandboxConfig?.filesystem?.allowWrite ?? []);
360
for (const defaultAllowWrite of this._getDefaultAllowWrite(launchCwd ? [launchCwd] : undefined)) {
361
if (defaultAllowWrite) {
362
mergedAllowWrite.add(defaultAllowWrite);
363
}
364
}
365
366
const mergedAllowedDomains = new Set(sandboxConfig?.network?.allowedDomains ?? []);
367
for (const defaultAllowedDomain of this._defaultAllowedDomains) {
368
if (defaultAllowedDomain) {
369
mergedAllowedDomains.add(defaultAllowedDomain);
370
}
371
}
372
373
return {
374
...sandboxConfig,
375
network: {
376
allowedDomains: [...mergedAllowedDomains],
377
deniedDomains: sandboxConfig?.network?.deniedDomains ?? [],
378
},
379
filesystem: {
380
allowWrite: [...mergedAllowWrite],
381
denyRead: sandboxConfig?.filesystem?.denyRead ?? [],
382
denyWrite: sandboxConfig?.filesystem?.denyWrite ?? [],
383
},
384
};
385
}
386
387
private _getDefaultAllowWrite(directories?: string[]): readonly string[] {
388
for (const launchCwd of directories ?? []) {
389
const trimmed = launchCwd.trim();
390
if (trimmed) {
391
this._defaultAllowWritePaths.push(trimmed);
392
}
393
}
394
return this._defaultAllowWritePaths;
395
}
396
397
private _pathJoin = (os: OperatingSystem, ...segments: string[]) => {
398
const path = os === OperatingSystem.Windows ? win32 : posix;
399
return path.join(...segments);
400
};
401
402
private _getPathDelimiter = async (remoteAuthority?: string) => {
403
const os = await this._getOperatingSystem(remoteAuthority);
404
return os === OperatingSystem.Windows ? win32.delimiter : posix.delimiter;
405
};
406
407
private _quoteShellArgument(value: string): string {
408
return `'${value.replace(/'/g, `'\\''`)}'`;
409
}
410
411
}
412
413