Skip to content

Commit 4a95233

Browse files
authored
Enabling sandboxing for local MCP servers at workspace level. (#295704)
* Enabling MCP sandboxing * Added debug logs for troubleshooting * changes for confirmation window during MCP server run * refactoring changes * refactoring changes * code review comments * code review comments * code review comments * code review comments * fixing tests * Code review comments * code review * Code review comments
1 parent c3d3f8d commit 4a95233

File tree

11 files changed

+435
-20
lines changed

11 files changed

+435
-20
lines changed

src/vs/platform/mcp/common/mcpPlatformTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ export interface IMcpDevModeConfig {
1212
debug?: { type: 'node' } | { type: 'debugpy'; debugpyPath?: string };
1313
}
1414

15+
export interface IMcpSandboxConfiguration {
16+
network?: {
17+
allowedDomains?: string[];
18+
deniedDomains?: string[];
19+
};
20+
filesystem?: {
21+
denyRead?: string[];
22+
allowWrite?: string[];
23+
denyWrite?: string[];
24+
};
25+
}
26+
1527
export const enum McpServerVariableType {
1628
PROMPT = 'promptString',
1729
PICK = 'pickString',
@@ -45,6 +57,8 @@ export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfigurat
4557
readonly env?: Record<string, string | number | null>;
4658
readonly envFile?: string;
4759
readonly cwd?: string;
60+
readonly sandboxEnabled?: boolean;
61+
readonly sandbox?: IMcpSandboxConfiguration;
4862
readonly dev?: IMcpDevModeConfig;
4963
}
5064

src/vs/platform/mcp/common/mcpResourceScannerService.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
1818
import { createDecorator } from '../../instantiation/common/instantiation.js';
1919
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
2020
import { IInstallableMcpServer } from './mcpManagement.js';
21-
import { ICommonMcpServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js';
21+
import { ICommonMcpServerConfiguration, IMcpSandboxConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js';
2222

2323
interface IScannedMcpServers {
2424
servers?: IStringDictionary<Mutable<IMcpServerConfiguration>>;
2525
inputs?: IMcpServerVariable[];
26+
sandbox?: IMcpSandboxConfiguration;
2627
}
2728

2829
interface IOldScannedMcpServer {
@@ -77,7 +78,7 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc
7778
updatedInputs = [...updatedInputs, ...newInputs];
7879
}
7980
}
80-
return { servers: existingServers, inputs: updatedInputs };
81+
return { servers: existingServers, inputs: updatedInputs, sandbox: scannedMcpServers.sandbox };
8182
});
8283
}
8384

@@ -173,33 +174,35 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc
173174

174175
private fromUserMcpServers(scannedMcpServers: IScannedMcpServers): IScannedMcpServers {
175176
const userMcpServers: IScannedMcpServers = {
176-
inputs: scannedMcpServers.inputs
177+
inputs: scannedMcpServers.inputs,
178+
sandbox: scannedMcpServers.sandbox
177179
};
178180
const servers = Object.entries(scannedMcpServers.servers ?? {});
179181
if (servers.length > 0) {
180182
userMcpServers.servers = {};
181183
for (const [serverName, server] of servers) {
182-
userMcpServers.servers[serverName] = this.sanitizeServer(server);
184+
userMcpServers.servers[serverName] = this.sanitizeServer(server, scannedMcpServers.sandbox);
183185
}
184186
}
185187
return userMcpServers;
186188
}
187189

188190
private fromWorkspaceFolderMcpServers(scannedWorkspaceFolderMcpServers: IScannedMcpServers): IScannedMcpServers {
189191
const scannedMcpServers: IScannedMcpServers = {
190-
inputs: scannedWorkspaceFolderMcpServers.inputs
192+
inputs: scannedWorkspaceFolderMcpServers.inputs,
193+
sandbox: scannedWorkspaceFolderMcpServers.sandbox
191194
};
192195
const servers = Object.entries(scannedWorkspaceFolderMcpServers.servers ?? {});
193196
if (servers.length > 0) {
194197
scannedMcpServers.servers = {};
195198
for (const [serverName, config] of servers) {
196-
scannedMcpServers.servers[serverName] = this.sanitizeServer(config);
199+
scannedMcpServers.servers[serverName] = this.sanitizeServer(config, scannedWorkspaceFolderMcpServers.sandbox);
197200
}
198201
}
199202
return scannedMcpServers;
200203
}
201204

202-
private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable<IMcpServerConfiguration>): IMcpServerConfiguration {
205+
private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable<IMcpServerConfiguration>, sandbox?: IMcpSandboxConfiguration): IMcpServerConfiguration {
203206
let server: IMcpServerConfiguration;
204207
if ((<IOldScannedMcpServer>serverOrConfig).config) {
205208
const oldScannedMcpServer = <IOldScannedMcpServer>serverOrConfig;
@@ -216,6 +219,10 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc
216219
(<Mutable<ICommonMcpServerConfiguration>>server).type = (<IMcpStdioServerConfiguration>server).command ? McpServerType.LOCAL : McpServerType.REMOTE;
217220
}
218221

222+
if (sandbox && server.type === McpServerType.LOCAL && !(server as IMcpStdioServerConfiguration).sandbox && server.sandboxEnabled) {
223+
(<Mutable<IMcpStdioServerConfiguration>>server).sandbox = sandbox;
224+
}
225+
219226
return server;
220227
}
221228

src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { IMcpDevModeDebugging, McpDevModeDebugging } from '../common/mcpDevMode.
3030
import { McpLanguageModelToolContribution } from '../common/mcpLanguageModelToolContribution.js';
3131
import { McpRegistry } from '../common/mcpRegistry.js';
3232
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
33+
import { IMcpSandboxService, McpSandboxService } from '../common/mcpSandboxService.js';
3334
import { McpResourceFilesystem } from '../common/mcpResourceFilesystem.js';
3435
import { McpSamplingService } from '../common/mcpSamplingService.js';
3536
import { McpService } from '../common/mcpService.js';
@@ -50,6 +51,7 @@ import { McpServersViewsContribution } from './mcpServersView.js';
5051
import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchService.js';
5152

5253
registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed);
54+
registerSingleton(IMcpSandboxService, McpSandboxService, InstantiationType.Delayed);
5355
registerSingleton(IMcpService, McpService, InstantiationType.Delayed);
5456
registerSingleton(IMcpWorkbenchService, McpWorkbenchService, InstantiationType.Eager);
5557
registerSingleton(IMcpDevModeDebugging, McpDevModeDebugging, InstantiationType.Delayed);

src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc
102102
id: `${collectionId}.${server.name}`,
103103
label: server.name,
104104
launch,
105+
sandboxEnabled: config.type === 'http' ? undefined : config.sandboxEnabled,
106+
sandbox: config.type === 'http' || !config.sandboxEnabled ? undefined : config.sandbox,
105107
cacheNonce: await McpServerLaunch.hash(launch),
106108
roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,
107109
variableReplacement: {

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ export const mcpStdioServerSchema: IJSONSchema = {
136136
enum: ['stdio'],
137137
description: localize('app.mcp.json.type', "The type of the server.")
138138
},
139+
sandboxEnabled: {
140+
type: 'boolean',
141+
default: false,
142+
description: localize('app.mcp.json.sandboxEnabled', "Whether to run the server in a sandboxed environment.")
143+
},
139144
command: {
140145
type: 'string',
141146
description: localize('app.mcp.json.command', "The command to run the server.")
@@ -179,6 +184,50 @@ export const mcpServerSchema: IJSONSchema = {
179184
allowComments: true,
180185
additionalProperties: false,
181186
properties: {
187+
sandbox: {
188+
description: localize('app.mcp.json.sandbox', "Default sandbox settings for running servers."),
189+
type: 'object',
190+
additionalProperties: false,
191+
properties: {
192+
network: {
193+
type: 'object',
194+
additionalProperties: false,
195+
properties: {
196+
allowedDomains: {
197+
type: 'array',
198+
items: { type: 'string' },
199+
default: []
200+
},
201+
deniedDomains: {
202+
type: 'array',
203+
items: { type: 'string' },
204+
default: []
205+
}
206+
}
207+
},
208+
filesystem: {
209+
type: 'object',
210+
additionalProperties: false,
211+
properties: {
212+
denyRead: {
213+
type: 'array',
214+
items: { type: 'string' },
215+
default: []
216+
},
217+
allowWrite: {
218+
type: 'array',
219+
items: { type: 'string' },
220+
default: []
221+
},
222+
denyWrite: {
223+
type: 'array',
224+
items: { type: 'string' },
225+
default: []
226+
}
227+
}
228+
}
229+
}
230+
},
182231
servers: {
183232
examples: [
184233
mcpSchemaExampleServers,

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { IMcpRegistry } from './mcpRegistryTypes.js';
3030
import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType, McpToolVisibility } from './mcpTypes.js';
3131
import { mcpServerToSourceData } from './mcpTypesUtils.js';
3232
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
33+
import { McpServer } from './mcpServer.js';
3334

3435
interface ISyncedToolData {
3536
toolData: IToolData;
@@ -205,6 +206,11 @@ class McpToolImplementation implements IToolImpl {
205206
async prepareToolInvocation(context: IToolInvocationPreparationContext): Promise<IPreparedToolInvocation> {
206207
const tool = this._tool;
207208
const server = this._server;
209+
// ToDO: need to be revisited as the first tool invocation doesnt have sandbox info and we are optimistically assuming it is not sandboxed. We should ideally have the sandbox info.
210+
const sandboxEnabled = await McpServer.callOn(server, async (_handler, connection) => {
211+
return connection.definition.sandboxEnabled;
212+
});
213+
const isSandboxedServer = sandboxEnabled === true;
208214

209215
const mcpToolWarning = localize(
210216
'mcp.tool.warning',
@@ -215,15 +221,18 @@ class McpToolImplementation implements IToolImpl {
215221
// duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813
216222
const title = tool.definition.annotations?.title || tool.definition.title || ('`' + tool.definition.name + '`');
217223

218-
const confirm: IToolConfirmationMessages = {};
219-
if (!tool.definition.annotations?.readOnlyHint) {
220-
confirm.title = new MarkdownString(localize('msg.title', "Run {0}", title));
221-
confirm.message = new MarkdownString(tool.definition.description, { supportThemeIcons: true });
222-
confirm.disclaimer = mcpToolWarning;
223-
confirm.allowAutoConfirm = true;
224-
}
225-
if (tool.definition.annotations?.openWorldHint) {
226-
confirm.confirmResults = true;
224+
let confirm: IToolConfirmationMessages | undefined;
225+
if (!isSandboxedServer) {
226+
confirm = {};
227+
if (!tool.definition.annotations?.readOnlyHint) {
228+
confirm.title = new MarkdownString(localize('msg.title', "Run {0}", title));
229+
confirm.message = new MarkdownString(tool.definition.description, { supportThemeIcons: true });
230+
confirm.disclaimer = mcpToolWarning;
231+
confirm.allowAutoConfirm = true;
232+
}
233+
if (tool.definition.annotations?.openWorldHint) {
234+
confirm.confirmResults = true;
235+
}
227236
}
228237

229238
const mcpUiEnabled = this._configurationService.getValue<boolean>(mcpAppsEnabledConfig);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/commo
3232
import { IMcpDevModeDebugging } from './mcpDevMode.js';
3333
import { McpRegistryInputStorage } from './mcpRegistryInputStorage.js';
3434
import { IMcpHostDelegate, IMcpRegistry, IMcpResolveConnectionOptions } from './mcpRegistryTypes.js';
35+
import { IMcpSandboxService } from './mcpSandboxService.js';
3536
import { McpServerConnection } from './mcpServerConnection.js';
3637
import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpDefinitionReference, McpServerDefinition, McpServerLaunch, McpServerTrust, McpStartServerInteraction, UserInteractionRequiredError } from './mcpTypes.js';
3738

@@ -85,6 +86,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
8586
@IQuickInputService private readonly _quickInputService: IQuickInputService,
8687
@ILabelService private readonly _labelService: ILabelService,
8788
@ILogService private readonly _logService: ILogService,
89+
@IMcpSandboxService private readonly _mcpSandboxService: IMcpSandboxService,
8890
) {
8991
super();
9092
this._mcpAccessValue = observableConfigValue(mcpAccessConfig, McpAccessValue.All, configurationService);
@@ -509,6 +511,8 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
509511
if (definition.devMode && debug) {
510512
launch = await this._instantiationService.invokeFunction(accessor => accessor.get(IMcpDevModeDebugging).transform(definition, launch!));
511513
}
514+
// If sandbox is enabled for this server, attempt to launch in sandbox
515+
launch = await this._mcpSandboxService.launchInSandboxIfEnabled(definition, launch, collection.remoteAuthority ?? undefined, collection.configTarget);
512516
} catch (e) {
513517
if (e instanceof UserInteractionRequiredError) {
514518
throw e;

0 commit comments

Comments
 (0)