Skip to content

Commit 093bcbf

Browse files
raymondginger2018-sudoRaymo
authored andcommitted
feat(mcp): strict MCP config mode (launcher allowlist)
When settings.strictMcpConfig (or STRICT_MCP_CONFIG env) is enabled, MCP servers may only be started by allowlisted launcher commands (npx/node/ python/uvx/bun/deno/go/java/...). A disallowed command fails fast with a clear status before anything is spawned, so an untrusted config cannot run arbitrary executables. Wired through SessionManager.initMcpServers. Extracted from the earlier closed PR #263 (kept as a focused change).
1 parent c5d8956 commit 093bcbf

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

packages/core/src/mcp/mcp-manager.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ const MCP_CALL_TOOL_TIMEOUT_MS = 60_000;
99
const API_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
1010
const API_TOOL_NAME_MAX_LENGTH = 64;
1111

12+
/**
13+
* Strict MCP config mode allowlist. When `strictMcpConfig` is enabled, only
14+
* these well-known launcher commands may start an MCP server; any other
15+
* command fails fast with a clear status instead of running arbitrary
16+
* executables. Mirrors the upstream `--strict-mcp-config` behavior.
17+
*/
18+
const MCP_STRICT_ALLOWLIST_COMMANDS = new Set([
19+
"npx",
20+
"node",
21+
"python3",
22+
"python",
23+
"uvx",
24+
"uv",
25+
"bun",
26+
"deno",
27+
"go",
28+
"java",
29+
]);
30+
1231
type McpToolEntry = {
1332
serverName: string;
1433
originalName: string;
@@ -78,6 +97,12 @@ export class McpManager {
7897
private onToolsListChanged: (() => void) | null = null;
7998
private onStatusChanged: (() => void) | null = null;
8099
private serverConfigs: Record<string, McpServerConfig> = {};
100+
/** Strict mode: only allowlisted launcher commands may start MCP servers. */
101+
private strictMode = false;
102+
103+
setStrictMode(enabled: boolean): void {
104+
this.strictMode = enabled;
105+
}
81106

82107
prepare(servers?: Record<string, McpServerConfig>): void {
83108
if (!servers || Object.keys(servers).length === 0) return;
@@ -146,6 +171,31 @@ export class McpManager {
146171
private async connectServer(name: string, config: McpServerConfig): Promise<void> {
147172
if (this.disposed) return;
148173

174+
// Strict mode: validate the launcher command against the allowlist before
175+
// spawning anything, so an unknown executable never runs.
176+
if (this.strictMode) {
177+
const commandName = config.command.split(/[\\/]/).pop() ?? config.command;
178+
if (!MCP_STRICT_ALLOWLIST_COMMANDS.has(commandName)) {
179+
const msg =
180+
`Strict MCP config: command "${config.command}" is not in the allowlist. ` +
181+
`Allowed commands: ${[...MCP_STRICT_ALLOWLIST_COMMANDS].join(", ")}. ` +
182+
`Disable strictMcpConfig in settings.json to bypass.`;
183+
this.setStatus({
184+
name,
185+
status: "failed",
186+
connected: false,
187+
error: msg,
188+
toolCount: 0,
189+
tools: [],
190+
promptCount: 0,
191+
prompts: [],
192+
resourceCount: 0,
193+
resources: [],
194+
});
195+
return;
196+
}
197+
}
198+
149199
// Clean up stale entries from previous connection attempts
150200
this.clients = this.clients.filter((c) => c.isConnected());
151201
this.tools = this.tools.filter((t) => t.serverName !== name);

packages/core/src/session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ export class SessionManager {
400400
autoCompactWindow?: number;
401401
webSearchTool?: string;
402402
mcpServers?: Record<string, McpServerConfig>;
403+
strictMcpConfig?: boolean;
403404
permissions?: Required<PermissionSettings>;
404405
enabledSkills?: Record<string, boolean>;
405406
};
@@ -451,6 +452,7 @@ export class SessionManager {
451452
}
452453

453454
async initMcpServers(servers?: Record<string, McpServerConfig>): Promise<void> {
455+
this.mcpManager.setStrictMode(this.getResolvedSettings().strictMcpConfig ?? false);
454456
this.mcpManager.setOnToolsListChanged(() => {
455457
this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
456458
});

packages/core/src/settings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export type DeepcodingSettings = {
101101
fileQuotaCleanupBatch?: number;
102102
maxRequestFilesBytes?: number;
103103
mcpServers?: Record<string, McpServerConfig>;
104+
/** When true, MCP server launcher commands are restricted to an allowlist. */
105+
strictMcpConfig?: boolean;
104106
permissions?: PermissionSettings;
105107
enabledSkills?: EnabledSkillsSettings;
106108
statusline?: StatusLineSettings;
@@ -128,6 +130,7 @@ export type ResolvedDeepcodingSettings = {
128130
fileQuotaCleanupBatch: number;
129131
maxRequestFilesBytes: number;
130132
mcpServers?: Record<string, McpServerConfig>;
133+
strictMcpConfig: boolean;
131134
permissions: Required<PermissionSettings>;
132135
enabledSkills: EnabledSkillsSettings;
133136
statusline: ResolvedStatusLineSettings;
@@ -632,6 +635,11 @@ export function resolveSettingsSources(
632635
trimString(projectSettings?.webSearchTool) ||
633636
trimString(userSettings?.webSearchTool) ||
634637
"";
638+
const strictMcpConfig =
639+
parseBoolean(systemEnv.STRICT_MCP_CONFIG) ??
640+
parseBoolean(projectSettings?.strictMcpConfig) ??
641+
parseBoolean(userSettings?.strictMcpConfig) ??
642+
false;
635643

636644
const multimodal =
637645
resolveMultimodalMode(systemEnv.MULTIMODAL) ??
@@ -697,6 +705,7 @@ export function resolveSettingsSources(
697705
fileQuotaCleanupBatch,
698706
maxRequestFilesBytes,
699707
mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv),
708+
strictMcpConfig,
700709
permissions: mergePermissions(userSettings, projectSettings),
701710
enabledSkills: mergeEnabledSkills(userSettings, projectSettings),
702711
statusline: mergeStatusLine(userSettings, projectSettings),

0 commit comments

Comments
 (0)