From 8047c321cb21103071e880f52359c68f35728b44 Mon Sep 17 00:00:00 2001 From: Ramiro Rivera Date: Sat, 1 Aug 2026 12:53:24 +0200 Subject: [PATCH] fix(selfhost): opt in stdio MCP via environment --- apps/host-selfhost/.env.example | 6 ++ apps/host-selfhost/executor.config.ts | 12 ++-- .../host-selfhost/src/executor-config.test.ts | 59 +++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 apps/host-selfhost/src/executor-config.test.ts diff --git a/apps/host-selfhost/.env.example b/apps/host-selfhost/.env.example index 659751596..936d51ff6 100644 --- a/apps/host-selfhost/.env.example +++ b/apps/host-selfhost/.env.example @@ -30,3 +30,9 @@ # Allow sandboxed code to reach loopback/private network addresses. Off by # default — adversarial generated code should not reach your internal network. # EXECUTOR_ALLOW_LOCAL_NETWORK=false + +# --- Local stdio MCP (trusted deployments only) ------------------------------- +# Stdio MCP is disabled unless this is explicitly set to the exact string +# "true". Enabling it lets users configure MCP servers whose commands execute +# on this host. +# EXECUTOR_ALLOW_STDIO_MCP=true diff --git a/apps/host-selfhost/executor.config.ts b/apps/host-selfhost/executor.config.ts index d09d5c1b6..c0826b9bc 100644 --- a/apps/host-selfhost/executor.config.ts +++ b/apps/host-selfhost/executor.config.ts @@ -19,10 +19,10 @@ import { resolveSecretKey } from "./src/config"; // Single source of truth for the self-hosted app's plugin list. // // Self-host runs the same protocol/provider plugins as cloud, minus the -// multi-tenant-only secret backends (WorkOS Vault). `dangerouslyAllowStdioMCP` -// is false: a server reachable by multiple users must not let one user spawn -// arbitrary stdio MCP processes on the host. The encrypted DB secret provider -// (slice 4) is added here as the first writable secret provider. +// multi-tenant-only secret backends (WorkOS Vault). Stdio MCP remains opt-in: +// a server reachable by multiple users must not let one user spawn arbitrary +// stdio MCP processes on the host. The encrypted DB secret provider (slice 4) +// is added here as the first writable secret provider. // --------------------------------------------------------------------------- interface SelfHostPluginDeps { @@ -39,7 +39,9 @@ export default defineExecutorConfig({ presets: [...googleCatalog, ...microsoftCatalog], specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter], }), - mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }), + mcpHttpPlugin({ + dangerouslyAllowStdioMCP: process.env.EXECUTOR_ALLOW_STDIO_MCP === "true", + }), graphqlHttpPlugin(), toolkitsPlugin({ activeToolkitSlug }), // First writable secret provider -> the default for `secrets.set`. diff --git a/apps/host-selfhost/src/executor-config.test.ts b/apps/host-selfhost/src/executor-config.test.ts new file mode 100644 index 000000000..0d56bc32f --- /dev/null +++ b/apps/host-selfhost/src/executor-config.test.ts @@ -0,0 +1,59 @@ +import { afterEach, beforeEach, expect, test } from "@effect/vitest"; + +import executorConfig from "../executor.config"; + +const ENV_NAME = "EXECUTOR_ALLOW_STDIO_MCP"; +const SECRET_ENV_NAME = "EXECUTOR_SECRET_KEY"; +const originalValue = process.env[ENV_NAME]; +const originalSecret = process.env[SECRET_ENV_NAME]; + +beforeEach(() => { + process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret"; +}); + +afterEach(() => { + if (originalValue === undefined) { + delete process.env[ENV_NAME]; + } else { + process.env[ENV_NAME] = originalValue; + } + if (originalSecret === undefined) { + delete process.env[SECRET_ENV_NAME]; + } else { + process.env[SECRET_ENV_NAME] = originalSecret; + } +}); + +const allowStdio = (): boolean => { + const mcp = executorConfig.plugins().find((plugin) => plugin.id === "mcp"); + expect(mcp).toBeDefined(); + const clientConfig = mcp?.clientConfig; + if ( + clientConfig && + typeof clientConfig === "object" && + "allowStdio" in clientConfig && + typeof clientConfig.allowStdio === "boolean" + ) { + return clientConfig.allowStdio; + } + expect.fail("MCP plugin did not expose its stdio setting"); + return false; +}; + +test("stdio MCP stays disabled when the opt-in is absent", () => { + delete process.env[ENV_NAME]; + expect(allowStdio()).toBe(false); +}); + +test("stdio MCP stays disabled unless the opt-in is exactly true", () => { + process.env[ENV_NAME] = "false"; + expect(allowStdio()).toBe(false); + + process.env[ENV_NAME] = "TRUE"; + expect(allowStdio()).toBe(false); +}); + +test("stdio MCP is enabled when the opt-in is exactly true", () => { + process.env[ENV_NAME] = "true"; + expect(allowStdio()).toBe(true); +});