src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts

415 LOC · 114 covered · 301 uncovered · 20 ranges · 111 concepts · 1 introducers · 74 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

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