export interface Config { port: number; nodeEnv: "production" | "development" | "test"; authToken: string | null; metricsToken: string | null; allowedHosts: string[]; allowedOrigins: string[]; buildSha: string; // Limits maxSessions: number; maxActiveExecs: number; maxSessionFsBytes: number; maxSingleFileBytes: number; maxWriteBatchBytes: number; maxReadBytesPerCall: number; maxEmbeddedStdoutBytes: number; maxEmbeddedStderrBytes: number; maxCapturedOutputBytesExec: number; maxResourceBytesSession: number; maxResourceBytesGlobal: number; maxExecsRetainedSession: number; workerMaxOldGenMb: number; // TTLs and timeouts sessionIdleTtlSeconds: number; sessionSweepIntervalSeconds: number; defaultTimeoutMs: number; maxTimeoutMs: number; watchdogGraceMs: number; shutdownGraceMs: number; // Capabilities enableNetwork: boolean; enablePython: boolean; enableJavaScript: boolean; dangerouslyAllowFullInternetAccess: boolean; allowedUrlPrefixes: string[]; allowedMethods: string[]; // Rate limits maxSessionsPerToken: number; maxActiveExecsPerToken: number; maxToolCallsPerMinute: number; maxAuthFailuresPerMinute: number; } function num(name: string, def: number): number { const v = process.env[name]; if (v == null || v === "") return def; const n = Number(v); if (!Number.isFinite(n)) { throw new Error(`${name} must be a number, got: ${v}`); } return n; } function bool(name: string, def: boolean): boolean { const v = process.env[name]; if (v == null || v === "") return def; return v === "true" || v === "1"; } function csv(name: string, def: string[]): string[] { const v = process.env[name]; if (v == null || v === "") return def; return v .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); } export function loadConfig(): Config { const nodeEnv = (process.env.NODE_ENV as Config["nodeEnv"]) || "development"; return { port: num("PORT", 7860), nodeEnv, authToken: process.env.MCP_AUTH_TOKEN || null, metricsToken: process.env.METRICS_TOKEN || null, allowedHosts: csv("ALLOWED_HOSTS", ["localhost", "127.0.0.1", "0.0.0.0"]), allowedOrigins: csv("ALLOWED_ORIGINS", []), buildSha: process.env.BUILD_SHA || "dev", maxSessions: num("MAX_SESSIONS", 10), maxActiveExecs: num("MAX_ACTIVE_EXECS", 2), maxSessionFsBytes: num("MAX_SESSION_FS_BYTES", 10 * 1024 * 1024), maxSingleFileBytes: num("MAX_SINGLE_FILE_BYTES", 5 * 1024 * 1024), maxWriteBatchBytes: num("MAX_WRITE_BATCH_BYTES", 2 * 1024 * 1024), maxReadBytesPerCall: num("MAX_READ_BYTES_PER_CALL", 64 * 1024), maxEmbeddedStdoutBytes: num("MAX_EMBEDDED_STDOUT_BYTES", 16 * 1024), maxEmbeddedStderrBytes: num("MAX_EMBEDDED_STDERR_BYTES", 8 * 1024), maxCapturedOutputBytesExec: num("MAX_CAPTURED_OUTPUT_BYTES_EXEC", 512 * 1024), maxResourceBytesSession: num("MAX_RESOURCE_BYTES_SESSION", 2 * 1024 * 1024), maxResourceBytesGlobal: num("MAX_RESOURCE_BYTES_GLOBAL", 32 * 1024 * 1024), maxExecsRetainedSession: num("MAX_EXECS_RETAINED_SESSION", 20), workerMaxOldGenMb: num("WORKER_MAX_OLD_GEN_MB", 64), sessionIdleTtlSeconds: num("SESSION_IDLE_TTL_SECONDS", 1800), sessionSweepIntervalSeconds: num("SESSION_SWEEP_INTERVAL_SECONDS", 60), defaultTimeoutMs: num("DEFAULT_TIMEOUT_MS", 5000), maxTimeoutMs: num("MAX_TIMEOUT_MS", 30000), watchdogGraceMs: num("WATCHDOG_GRACE_MS", 1000), shutdownGraceMs: num("SHUTDOWN_GRACE_MS", 5000), enableNetwork: bool("ENABLE_NETWORK", false), enablePython: bool("ENABLE_PYTHON", false), enableJavaScript: bool("ENABLE_JAVASCRIPT", false), dangerouslyAllowFullInternetAccess: bool( "DANGEROUSLY_ALLOW_FULL_INTERNET_ACCESS", false, ), allowedUrlPrefixes: csv("ALLOWED_URL_PREFIXES", []), allowedMethods: csv("ALLOWED_METHODS", ["GET", "HEAD"]), maxSessionsPerToken: num("MAX_SESSIONS_PER_TOKEN", 10), maxActiveExecsPerToken: num("MAX_ACTIVE_EXECS_PER_TOKEN", 2), maxToolCallsPerMinute: num("MAX_TOOL_CALLS_PER_MINUTE", 60), maxAuthFailuresPerMinute: num("MAX_AUTH_FAILURES_PER_MINUTE", 10), }; } export interface ValidationIssue { level: "error" | "warn"; message: string; } export function validateConfig(cfg: Config): ValidationIssue[] { const issues: ValidationIssue[] = []; if (cfg.nodeEnv === "production") { if (!cfg.authToken) { issues.push({ level: "warn", message: "MCP_AUTH_TOKEN is not set — /mcp is open to any caller. Set ALLOW_PUBLIC=true to acknowledge.", }); if (process.env.ALLOW_PUBLIC !== "true") { issues.push({ level: "error", message: "Refusing to start with no MCP_AUTH_TOKEN in production. Set ALLOW_PUBLIC=true to allow.", }); } } else if (Buffer.byteLength(cfg.authToken, "utf8") < 32) { issues.push({ level: "error", message: "MCP_AUTH_TOKEN must be at least 32 bytes in production", }); } if (cfg.allowedHosts.length === 0) { issues.push({ level: "error", message: "ALLOWED_HOSTS must be non-empty in production" }); } else if (cfg.allowedHosts.includes("*")) { issues.push({ level: "warn", message: "ALLOWED_HOSTS=* — host validation is disabled", }); } if (cfg.allowedOrigins.length === 0) { issues.push({ level: "error", message: "ALLOWED_ORIGINS must be non-empty in production" }); } else if (cfg.allowedOrigins.includes("*")) { issues.push({ level: "warn", message: "ALLOWED_ORIGINS=* — origin validation is disabled", }); } if (cfg.enableNetwork) { issues.push({ level: "warn", message: cfg.dangerouslyAllowFullInternetAccess ? "ENABLE_NETWORK + DANGEROUSLY_ALLOW_FULL_INTERNET_ACCESS: workers can reach any URL" : `ENABLE_NETWORK with allow-list (${cfg.allowedUrlPrefixes.length} prefix(es))`, }); } if (cfg.enablePython) { issues.push({ level: "warn", message: "ENABLE_PYTHON: python3/python available to scripts" }); } if (cfg.enableJavaScript) { issues.push({ level: "warn", message: "ENABLE_JAVASCRIPT: js-exec available to scripts via QuickJS", }); } if (cfg.dangerouslyAllowFullInternetAccess && !cfg.enableNetwork) { issues.push({ level: "error", message: "DANGEROUSLY_ALLOW_FULL_INTERNET_ACCESS requires ENABLE_NETWORK=true", }); } } if (cfg.allowedOrigins.some((o) => o.includes("*"))) { issues.push({ level: "warn", message: "Wildcard origin configured; this is weak defense-in-depth only", }); } const projectedMemoryMb = (cfg.maxSessions * cfg.workerMaxOldGenMb) + Math.ceil(cfg.maxResourceBytesGlobal / (1024 * 1024)); if (projectedMemoryMb > 16 * 1024) { issues.push({ level: "warn", message: `Projected memory budget (~${projectedMemoryMb}MB) exceeds 16GB; verify hardware profile`, }); } if (cfg.workerMaxOldGenMb <= 0) { issues.push({ level: "error", message: "WORKER_MAX_OLD_GEN_MB must be > 0" }); } if (cfg.port < 0 || cfg.port > 65535) { issues.push({ level: "error", message: `PORT must be 0..65535, got ${cfg.port}` }); } return issues; }