overmind-mcp
Version:
Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour
694 lines • 31.4 kB
JavaScript
import fs from 'fs';
import path from 'path';
import http from 'http';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import { PassThrough } from 'stream';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Initialize OVERMIND_WORKSPACE to project directory to prevent global .env overrides
process.env.OVERMIND_WORKSPACE = process.env.OVERMIND_WORKSPACE || path.resolve(__dirname, '../..');
function loadEnvQuietly(envPath) {
try {
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf8');
content.split('\n').forEach((line) => {
const trimmed = line.trim();
// Skip empty lines and full comment lines
if (!trimmed || trimmed.startsWith('#'))
return;
// Parse KEY=VALUE (value can contain '=')
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1)
return;
const key = trimmed.slice(0, eqIdx).trim();
if (!key)
return;
let value = trimmed.slice(eqIdx + 1).trim();
// Strip trailing comments only when preceded by whitespace (not in quoted values)
// e.g. FOO=hello # comment → strips "# comment"
// BAR=http://foo.com#fragment → preserves "#fragment"
if (!value.startsWith('"') && !value.startsWith("'")) {
value = value.replace(/\s+#.*$/, '').trim();
}
// Strip quotes
if (value.startsWith('"') && value.endsWith('"'))
value = value.slice(1, -1);
else if (value.startsWith("'") && value.endsWith("'"))
value = value.slice(1, -1);
if (!process.env[key])
process.env[key] = value;
});
}
}
catch (_e) {
// Ignore silently
}
}
// Atomic write: write to temp file first, then rename (atomic on POSIX, prevents corruption on crash)
function atomicWriteFile(filePath, content) {
const tmp = filePath + '.tmp.' + Math.random().toString(36).slice(2);
fs.writeFileSync(tmp, content, 'utf8');
fs.renameSync(tmp, filePath);
}
// Auto-formatteur de .env — Classifie tous les providers connus, préserve les valeurs
function autoFormatEnvFile(envPath) {
try {
if (!fs.existsSync(envPath))
return;
const content = fs.readFileSync(envPath, 'utf8');
// Parser robuste : ignore commentaires, lit les lignes KEY=VALUE (valeur peut contenir '=')
const envVars = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#'))
continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1)
continue;
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
if (key)
envVars[key] = value;
}
if (Object.keys(envVars).length === 0)
return;
const usedKeys = new Set();
// Tri naturel pour les clés avec suffixes numériques (KEY, KEY_2, KEY_3…)
function naturalSort(pairs) {
return pairs.sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true, sensitivity: 'base' }));
}
function extract(prefix, exactMatch = false) {
const res = [];
const prefixes = Array.isArray(prefix) ? prefix : [prefix];
for (const key of Object.keys(envVars)) {
if (usedKeys.has(key))
continue;
const matches = exactMatch
? prefixes.includes(key)
: prefixes.some((p) => key.startsWith(p));
if (matches) {
res.push([key, envVars[key]]);
usedKeys.add(key);
}
}
return naturalSort(res);
}
// ─── SECTIONS ──────────────────────────────────────────────────────────────
const sections = [
// ── Overmind Infrastructure ──
{
title: '🌐 OVERMIND CORE & INFRASTRUCTURE',
vars: extract(['OVERMIND_WORKSPACE', 'OVERMIND_MEMORY_TYPE'], true),
},
{ title: '⚙️ GLOBAL SETTINGS', vars: extract(['API_TIMEOUT_MS'], true) },
{ title: '🗄️ DATABASE (PostgreSQL / pgvector)', vars: extract('POSTGRES_') },
{ title: '🧠 EMBEDDINGS & VECTOR MEMORY', vars: extract('OVERMIND_EMBEDDING_') },
// ── LLM Providers (Europe / USA) ──
{ title: '🤖 LLM PROVIDER - Mistral AI 🇫🇷', vars: extract('MISTRAL_') },
{ title: '🤖 LLM PROVIDER - OpenAI 🇺🇸', vars: extract('OPENAI_') },
{ title: '🤖 LLM PROVIDER - Anthropic 🇺🇸', vars: extract('ANTHROPIC_') },
{
title: '🤖 LLM PROVIDER - Google Gemini 🇺🇸',
vars: extract(['GEMINI_API_KEY', 'GEMINI_MODEL', 'GOOGLE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY'], true),
},
{
title: '🤖 LLM PROVIDER - NVIDIA NIM 🇺🇸',
vars: [
...extract(['NVAPI_KEY', 'NVIDIA_API_KEY', 'NVIDIA_API_BASE'], true),
...extract('NVIDIA_'),
],
},
{
title: '🤖 LLM PROVIDER - OpenRouter 🇺🇸',
vars: extract(['OPENROUTER_API_KEY', 'OPENROUTER_BASE_URL', 'OPENROUTER_MODEL'], true),
},
{
title: '🤖 LLM PROVIDER - xAI / Grok 🇺🇸',
vars: extract(['XAI_API_KEY', 'XAI_BASE_URL', 'GROK_API_KEY', 'GROK_MODEL'], true),
},
{ title: '🤖 LLM PROVIDER - Groq 🇺🇸', vars: extract('GROQ_') },
{ title: '🤖 LLM PROVIDER - Together AI 🇺🇸', vars: extract('TOGETHER_') },
{ title: '🤖 LLM PROVIDER - Cohere 🇺🇸', vars: extract('COHERE_') },
{ title: '🤖 LLM PROVIDER - Replicate 🇺🇸', vars: extract('REPLICATE_') },
{
title: '🤖 LLM PROVIDER - HuggingFace 🇺🇸',
vars: extract(['HUGGINGFACE_API_KEY', 'HF_TOKEN', 'HUGGING_FACE_HUB_TOKEN'], true),
},
{ title: '🤖 LLM PROVIDER - Perplexity 🇺🇸', vars: extract('PERPLEXITY_') },
{ title: '🤖 LLM PROVIDER - SambaNova 🇺🇸', vars: extract('SAMBANOVA_') },
{
title: '🤖 LLM PROVIDER - Azure OpenAI ☁️',
vars: extract([
'AZURE_API_KEY',
'AZURE_API_BASE',
'AZURE_OPENAI_API_KEY',
'AZURE_OPENAI_ENDPOINT',
'AZURE_OPENAI_API_VERSION',
], true),
},
{
title: '🤖 LLM PROVIDER - ElevenLabs 🎙️',
vars: extract(['ELEVENLABS_API_KEY', 'ELEVENLABS_VOICE_ID', 'ELEVENLABS_MODEL_ID'], true),
},
{
title: '🤖 LLM PROVIDER - AWS Bedrock ☁️',
vars: extract([
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_SESSION_TOKEN',
'AWS_REGION',
'AWS_DEFAULT_REGION',
'AWS_ENDPOINT_URL',
], true),
},
// ── LLM Providers (Chine / Asie) ──
{ title: '🤖 LLM PROVIDER - DeepSeek 🇨🇳', vars: extract('DEEPSEEK_') },
{ title: '🤖 LLM PROVIDER - Alibaba DashScope 🇨🇳', vars: extract('ALIBABA_') },
{
title: '🤖 LLM PROVIDER - Qwen (DashScope) 🇨🇳',
vars: extract([
'QWEN_API_KEY',
'QWEN_BASE_URL',
'QWEN_MODEL',
'DASHSCOPE_API_KEY',
'DASHSCOPE_BASE_URL',
], true),
},
{ title: '🤖 LLM PROVIDER - SiliconFlow 🇨🇳', vars: extract('SILICONFLOW_') },
{
title: '🤖 LLM PROVIDER - Minimax 🇨🇳',
vars: [...extract('MINIMAXI_'), ...extract('MINIMAX_')],
},
{ title: '🤖 LLM PROVIDER - Ilmu AI / Z.AI 🇨🇳', vars: extract('Z_AI_') },
{
title: '🤖 LLM PROVIDER - Moonshot / Kimi 🇨🇳',
vars: extract(['MOONSHOT_API_KEY', 'MOONSHOT_BASE_URL', 'MOONSHOT_MODEL', 'KIMI_API_KEY'], true),
},
{
title: '🤖 LLM PROVIDER - Baidu / ERNIE 🇨🇳',
vars: extract(['BAIDU_API_KEY', 'ERNIE_API_KEY', 'WENXIN_API_KEY'], true),
},
{
title: '🤖 LLM PROVIDER - ZhipuAI / GLM 🇨🇳',
vars: [...extract(['ZHIPU_API_KEY', 'GLM_API_KEY'], true), ...extract('ZHIPUAI_')],
},
// ── Services & Intégrations ──
{
title: '💬 SERVICE - Discord',
vars: extract([
'DISCORD_TOKEN',
'DISCORD_BOT_TOKEN',
'DISCORD_CHANNEL_ID',
'DISCORD_GUILD_ID',
'DISCORD_WEBHOOK_URL',
'DISCORD_CLIENT_ID',
'DISCORD_CLIENT_SECRET',
], true),
},
{
title: '🐦 SERVICE - Twitter / X',
vars: extract([
'TWITTER_API_KEY',
'TWITTER_API_SECRET',
'TWITTER_ACCESS_TOKEN',
'TWITTER_ACCESS_SECRET',
'TWITTER_BEARER_TOKEN',
'X_API_KEY',
'X_BEARER_TOKEN',
'X_API_SECRET',
'X_ACCESS_TOKEN',
'X_ACCESS_SECRET',
], true),
},
{
title: '📱 SERVICE - Telegram',
vars: extract(['TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID'], true),
},
{
title: '📱 SERVICE - Twilio / SMS',
vars: extract([
'TWILIO_ACCOUNT_SID',
'TWILIO_AUTH_TOKEN',
'TWILIO_PHONE_NUMBER',
'TWILIO_API_KEY',
'TWILIO_API_SECRET',
], true),
},
{
title: '🐙 SERVICE - GitHub',
vars: extract([
'GITHUB_TOKEN',
'GITHUB_API_KEY',
'GH_TOKEN',
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
], true),
},
{
title: '▲ SERVICE - Vercel',
vars: extract([
'VERCEL_TOKEN',
'VERCEL_API_TOKEN',
'VERCEL_PROJECT_ID',
'VERCEL_ORG_ID',
'VERCEL_TEAM_ID',
], true),
},
{
title: '🔺 SERVICE - Supabase',
vars: extract(['SUPABASE_URL', 'SUPABASE_ANON_KEY', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_JWT_SECRET'], true),
},
{ title: '🎮 SERVICE - Riot Games', vars: extract(['RIOT_API_KEY', 'RIOT_REGION'], true) },
{
title: '🔍 SERVICE - Search (Serper/Tavily)',
vars: extract(['SERPER_API_KEY', 'TAVILY_API_KEY', 'SERPAPI_KEY', 'BRAVE_SEARCH_API_KEY'], true),
},
{
title: '📰 SERVICE - Market Data & News',
vars: extract([
'FINNHUB_API_KEY',
'NEWSAPI_KEY',
'ALPHAVANTAGE_API_KEY',
'POLYGON_API_KEY',
'TIINGO_API_KEY',
], true),
},
];
// Clés restantes non classifiées → section fourre-tout
const uncategorized = [];
for (const key of Object.keys(envVars)) {
if (!usedKeys.has(key))
uncategorized.push([key, envVars[key]]);
}
if (uncategorized.length > 0) {
sections.push({ title: '📁 AUTRES / NON-CLASSIFIÉS', vars: naturalSort(uncategorized) });
}
// ─── RENDU ──────────────────────────────────────────────────────────────────
let newContent = '';
for (const section of sections) {
if (section.vars.length > 0) {
newContent += `# ==========================================\n`;
newContent += `# ${section.title}\n`;
newContent += `# ==========================================\n`;
for (const [k, v] of section.vars) {
newContent += `${k}=${v}\n`;
}
newContent += `\n`;
}
}
// N'écrire sur le disque que si quelque chose a changé (optimisation I/O)
if (content.trim() !== newContent.trim()) {
atomicWriteFile(envPath, newContent.trim() + '\n');
}
}
catch (_e) {
// Ignorer silencieusement pour ne pas crasher le boot
}
}
const localEnvPath = path.resolve(__dirname, '../../.env');
// Auto-détection et injection de OVERMIND_WORKSPACE s'il est manquant — NE PAS écraser s'il existe déjà
try {
if (!fs.existsSync(localEnvPath)) {
const workspacePath = path.resolve(__dirname, '../../');
atomicWriteFile(localEnvPath, `OVERMIND_WORKSPACE=${workspacePath}\n`);
}
else {
const currentContent = fs.readFileSync(localEnvPath, 'utf8');
const match = currentContent.match(/^OVERMIND_WORKSPACE=(.*)$/m);
if (!match || !match[1]?.trim()) {
const workspacePath = path.resolve(__dirname, '../../');
if (!currentContent.includes('OVERMIND_WORKSPACE=')) {
atomicWriteFile(localEnvPath, currentContent.trim() + `\nOVERMIND_WORKSPACE=${workspacePath}\n`);
}
else {
const newContent = currentContent.replace(/^OVERMIND_WORKSPACE=.*$/m, `OVERMIND_WORKSPACE=${workspacePath}`);
atomicWriteFile(localEnvPath, newContent);
}
}
}
}
catch (_e) {
// Ignorer l'erreur silencieusement
}
// Classifie et réorganise automatiquement le .env à chaque démarrage d'Overmind
autoFormatEnvFile(localEnvPath);
// 🎯 Priorité de chargement (premier trouvé gagne, car loadEnvQuietly ne réécrit pas) :
// 1) $OVERMIND_ENV_FILE → fichier explicite passé par le client MCP
// 2) <process.cwd()>/.env → si lancé depuis un dossier projet
// 3) <bin>/../../.env → fallback historique (utile en dev local)
const externalEnvCandidates = [];
if (process.env.OVERMIND_ENV_FILE)
externalEnvCandidates.push(process.env.OVERMIND_ENV_FILE);
const cwdEnv = path.resolve(process.cwd(), '.env');
if (cwdEnv !== localEnvPath)
externalEnvCandidates.push(cwdEnv);
for (const candidate of externalEnvCandidates) {
loadEnvQuietly(candidate);
}
// Load OverMind's specific environment variables
loadEnvQuietly(localEnvPath);
// Inherit PostgreSQL Server environment variables (Database credentials, Embeddings)
loadEnvQuietly(path.resolve(__dirname, '../../../serveur_PostGreSQL/.env'));
// 🔍 Vérification des configurations critiques
function checkMissingConfigs() {
const missingCore = [];
const missingDb = [];
// Vérification CORE
if (!process.env.OVERMIND_WORKSPACE)
missingCore.push('OVERMIND_WORKSPACE');
if (!process.env.OVERMIND_MEMORY_TYPE)
missingCore.push('OVERMIND_MEMORY_TYPE');
// Vérification DB
if (!process.env.POSTGRES_HOST)
missingDb.push('POSTGRES_HOST');
if (!process.env.POSTGRES_DATABASE)
missingDb.push('POSTGRES_DATABASE');
if (!process.env.POSTGRES_USER)
missingDb.push('POSTGRES_USER');
if (!process.env.POSTGRES_PASSWORD)
missingDb.push('POSTGRES_PASSWORD');
if (missingCore.length > 0) {
console.error(`\n[Overmind] ⚠️ ATTENTION : Configuration 'CORE & INFRASTRUCTURE' incomplète !`);
console.error(`[Overmind] ❌ Il manque : ${missingCore.join(', ')}\n`);
}
if (missingDb.length > 0) {
console.error(`\n[Overmind] ⚠️ ATTENTION : Configuration 'DATABASE' incomplète (Imported from postgresql-server) !`);
console.error(`[Overmind] ❌ Il manque : ${missingDb.join(', ')}\n`);
}
}
checkMissingConfigs();
import { rootLogger } from '../lib/logger.js';
// 💥 ERROR HANDLING: Catch everything to avoid silent EOFs
process.on('uncaughtException', (err) => {
rootLogger.fatal({ err }, '💥 UNCAUGHT EXCEPTION');
rootLogger.flush();
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
rootLogger.fatal({ reason, promise }, '💥 UNHANDLED REJECTION');
rootLogger.flush();
});
// Ensure logs are flushed on exit
process.on('SIGINT', () => {
rootLogger.info('🛑 Received SIGINT, flushing logs...');
rootLogger.flush();
process.exit(0);
});
process.on('SIGTERM', () => {
rootLogger.info('🛑 Received SIGTERM, flushing logs...');
rootLogger.flush();
process.exit(0);
});
// 🛡️ SHIELD: Prevent any library from logging to stdout during initialization
// This is critical for MCP servers because any non-JSON output on stdout kills the handshake (EOF).
console.log = (...args) => rootLogger.info(args.length > 1 ? { args } : args[0]);
console.info = (...args) => rootLogger.info(args.length > 1 ? { args } : args[0]);
// 🛡️ E3: Warning filter — suppress repetitive FastMCP "could not infer client capabilities" spam.
// FastMCP retries client capability detection every ~180s in stateless HTTP mode, generating
// dozens of identical warnings. We log the FIRST occurrence, then suppress the rest.
const SUPPRESSED_WARN_PATTERNS = ['could not infer client capabilities'];
const loggedWarnings = new Set();
console.warn = (...args) => {
const msg = typeof args[0] === 'string' ? args[0] : String(args[0] ?? '');
const isSuppressed = SUPPRESSED_WARN_PATTERNS.some((p) => msg.includes(p));
if (isSuppressed) {
if (loggedWarnings.has(msg))
return; // Already logged once — suppress duplicate
loggedWarnings.add(msg);
rootLogger.debug({ suppressed: true }, `[WARN-DEDUP] ${msg}`);
return;
}
rootLogger.warn(args.length > 1 ? { args } : args[0]);
};
console.error = (...args) => rootLogger.error(args.length > 1 ? { args } : args[0]);
// 🛡️ ULTIMATE SHIELD: Proxy process.stdout.write to redirect non-JSON data to stderr
// E3: Deduplicate SHIELD warnings — library pino loggers fire repeatedly on stdout
const shieldDedup = new Set();
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = function (chunk, encoding, callback) {
const str = typeof chunk === 'string' ? chunk : chunk.toString();
const trimmed = str.trim();
// Handle overload: if encoding is a function, it's actually the callback
if (typeof encoding === 'function') {
callback = encoding;
encoding = undefined;
}
// Skip empty chunks or pure whitespace (often used as delimiters/keep-alive)
if (trimmed === '') {
return originalStdoutWrite(chunk, encoding, callback);
}
// Allow JSON-RPC (starts with { or [)
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
// BLOCK ARRAYS on stdout (Batch responses are not standard in MCP)
if (Array.isArray(parsed)) {
if (!shieldDedup.has('array-blocked')) {
shieldDedup.add('array-blocked');
rootLogger.warn({ raw: str }, '🛡️ [SHIELD] Blocked array-as-JSON-RPC on stdout (future occurrences suppressed)');
}
return process.stderr.write(chunk, encoding, callback);
}
// Check for JSON-RPC (Response or Notification)
if (parsed.jsonrpc === '2.0') {
return originalStdoutWrite(chunk, encoding, callback);
}
// Block non-RPC JSON (deduplicated — these fire repeatedly from library pino loggers)
if (!shieldDedup.has('object-blocked')) {
shieldDedup.add('object-blocked');
rootLogger.warn({ raw: str }, '🛡️ [SHIELD] Blocked non-JSON-RPC (Object) on stdout (future occurrences suppressed)');
}
return process.stderr.write(chunk, encoding, callback);
}
catch (e) {
// Malformed JSON-like content
rootLogger.debug({ raw: str, err: e.message }, '🛡️ [SHIELD] Blocked malformed JSON-like content on stdout');
return process.stderr.write(chunk, encoding, callback);
}
}
// Redirect everything else to stderr (via Pino)
rootLogger.debug({ raw: trimmed }, '🛡️ [SHIELD] Intercepted non-JSON stdout write');
return process.stderr.write(chunk, encoding, callback);
};
// 🛡️ STDIN INTERCEPTOR: Detect and unroll JSON-RPC Batches [req1, req2]
// FastMCP/Zod only support single objects. Intercepting before FastMCP starts.
const originalStdin = process.stdin;
const stdinProxy = new PassThrough();
originalStdin.on('data', (chunk) => {
const str = chunk.toString();
const trimmed = str.trim();
// 🔍 [DEBUG] Log every incoming chunk that looks like JSON
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
rootLogger.debug({
length: trimmed.length,
preview: trimmed.substring(0, 100),
isBatch: trimmed.startsWith('['),
}, '[SHIELD] STDIN chunk received');
}
// If it's a batch, unroll it into separate JSON chunks
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
rootLogger.warn({ count: parsed.length }, '[SHIELD] Unrolling JSON-RPC BATCH on STDIN');
for (const item of parsed) {
stdinProxy.write(JSON.stringify(item) + '\n');
}
return;
}
}
catch (_e) {
// Not a valid batch or partial JSON, pass through as-is
}
}
stdinProxy.write(chunk);
});
// Hijack process.stdin for FastMCP and any other consumers
Object.defineProperty(process, 'stdin', {
value: stdinProxy,
writable: false,
configurable: true,
});
// Setup completed - Dynamically import server components AFTER process.env is configured
const { initTelemetry } = await import('../lib/telemetry.js');
initTelemetry();
const { createServer } = await import('../server.js');
const { updateConfig } = await import('../lib/config.js');
const cliArgs = process.argv.slice(2);
let settingsPath, mcpPath;
let memoryOnly = false;
let memoryToolsOnly = false;
let sslCert;
let sslKey;
let sslCa;
let transportType = 'stdio';
let httpPort = 3099;
let httpHost = 'localhost';
let httpEndpoint = '/mcp';
for (let i = 0; i < cliArgs.length; i++) {
if (cliArgs[i] === '--settings' && cliArgs[i + 1]) {
settingsPath = cliArgs[i + 1];
i++;
}
else if (cliArgs[i] === '--mcp-config' && cliArgs[i + 1]) {
mcpPath = cliArgs[i + 1];
i++;
// ─── Mode restreint mémoire (v3.2 — actif via --memory-only) ────────────
// Le daemon écoute sur le même port que le serveur principal.
}
else if (cliArgs[i] === '--memory-only') {
memoryOnly = true;
}
else if (cliArgs[i] === '--memory-tools-only') {
memoryToolsOnly = true;
}
else if (cliArgs[i] === '--transport' && cliArgs[i + 1]) {
const t = cliArgs[i + 1];
if (t === 'http-stream' || t === 'httpStream') {
transportType = 'httpStream';
}
i++;
}
else if (cliArgs[i] === '--port' && cliArgs[i + 1]) {
httpPort = parseInt(cliArgs[i + 1], 10);
i++;
}
else if (cliArgs[i] === '--host' && cliArgs[i + 1]) {
httpHost = cliArgs[i + 1];
i++;
}
else if (cliArgs[i] === '--endpoint' && cliArgs[i + 1]) {
httpEndpoint = cliArgs[i + 1];
i++;
}
else if (cliArgs[i] === '--ssl-cert' && cliArgs[i + 1]) {
sslCert = cliArgs[i + 1];
i++;
}
else if (cliArgs[i] === '--ssl-key' && cliArgs[i + 1]) {
sslKey = cliArgs[i + 1];
i++;
}
else if (cliArgs[i] === '--ssl-ca' && cliArgs[i + 1]) {
sslCa = cliArgs[i + 1];
i++;
}
}
// Auth token (env var OR CLI flag). Si présent, toutes les requêtes HTTP
// doivent inclure "Authorization: Bearer <token>". Protège les 14 tools MCP.
const mcpAuthToken = process.env.OVERMIND_AUTH_TOKEN || undefined;
// ─── SECURITÉ: refuser bind réseau non-localhost sans SSL ───────────────────
// Un serveur HTTP sans chiffrement sur 0.0.0.0 expose les 14 tools MCP
// (run_agent = exécution de code arbitraire) à n'importe qui sur le LAN.
if (transportType === 'httpStream' && !(sslCert && sslKey)) {
const isLoopback = httpHost === 'localhost' || httpHost === '127.0.0.1' || httpHost === '::1';
if (!isLoopback) {
rootLogger.error(`[SECURITÉ] Refus de démarrer en HTTP (sans SSL) sur ${httpHost}.` +
` Utilisez --ssl-cert / --ssl-key ou bind sur localhost.`);
process.exit(1);
}
rootLogger.warn(`[SECURITÉ] Serveur HTTP sans SSL sur ${httpHost}.` +
` Acceptable en local, MAIS ne pas exposer sur le réseau sans --ssl-cert/--ssl-key.`);
}
if (settingsPath || mcpPath) {
updateConfig(settingsPath, mcpPath);
// Do NOT log to stderr during MCP initialization as it can cause EOF errors in some clients
}
// HTTP Singleton mode (OVERMIND_HTTP_MODE=true in .env)
// Les serveurs memory et postgresql tournent deja en singleton sur leurs ports,
// Overmind se configure en client HTTP pour aggreguer les outils
if (process.env.OVERMIND_HTTP_MODE === 'true') {
rootLogger.info('[Overmind] [HTTP] Mode singleton actif — memory sur port ' +
(process.env.MEMORY_HTTP_PORT || '3099') +
', postgresql sur port ' +
(process.env.POSTGRES_HTTP_PORT || '5433'));
rootLogger.info('[Overmind] [HTTP] Les outils sont exposés via la couche HTTP des serveurs distants');
// En mode HTTP singleton, Overmind CLI ne démarre pas son propre serveur MCP
// Il agit comme client pour les serveurs distants (memory + postgresql)
// Les agents se connectent directement aux endpoints HTTP des serveurs distants
process.exit(0);
}
const server = createServer('OverMind-MCP', memoryOnly, memoryToolsOnly);
rootLogger.info(memoryOnly
? '[Overmind] [START] Démarrage du serveur mémoire...'
: memoryToolsOnly
? '[Overmind] [START] Démarrage serveur (memory tools only)...'
: '[Overmind] [START] Démarrage du serveur...');
if (transportType === 'httpStream') {
const httpStreamConfig = {
port: httpPort,
host: httpHost,
endpoint: httpEndpoint,
stateless: true,
};
if (sslCert)
httpStreamConfig.sslCert = sslCert;
if (sslKey)
httpStreamConfig.sslKey = sslKey;
if (sslCa)
httpStreamConfig.sslCa = sslCa;
const protocol = sslCert && sslKey ? 'https' : 'http';
// PATCH: Monkey-patch http.createServer pour désactiver le requestTimeout de 5 min.
// Sans ça, Node tue les SSE streams des agents long-running après 300s.
// SCOPE: on restore l'original après la PREMIÈRE création de serveur,
// pour ne pas affecter les autres serveurs HTTP du process (bridge, etc.).
//
// AUTH: si mcpAuthToken est défini, on wrap le requestListener pour
// exiger "Authorization: Bearer *** sur chaque requête. Timing-safe
// compare pour éviter les timing attacks.
const origCreateServer = http.createServer.bind(http);
http.createServer = function (requestListener) {
// Wrap avec auth middleware si token configuré
const wrappedListener = mcpAuthToken
? (req, res) => {
// Permettre les requêtes SSE (GET) et POST sans auth pour l'initialization
// FastMCP fait l'initialize handshake avant les tool calls. On exige le
// token sur TOUTES les requêtes — le client doit l'envoyer dès le début.
const auth = req.headers['authorization'] || '';
const expected = `Bearer ${mcpAuthToken}`;
// Timing-safe comparison
const a = Buffer.from(auth);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unauthorized', code: -32000 }));
return;
}
if (typeof requestListener === 'function') {
requestListener(req, res);
}
}
: requestListener;
const hServer = origCreateServer(wrappedListener);
hServer.requestTimeout = 0;
hServer.headersTimeout = 0;
hServer.keepAliveTimeout = 0;
// Restaurer immédiatement — seul le serveur FastMCP doit être patché.
http.createServer = origCreateServer;
return hServer;
};
try {
await server.start({ transportType: 'httpStream', httpStream: httpStreamConfig });
rootLogger.info(`[Overmind] [READY] Serveur HTTP${sslCert ? 'S' : ''} sur ${protocol}://${httpHost}:${httpPort}${httpEndpoint}`);
}
catch (err) {
rootLogger.error({ error: err instanceof Error ? err.message : String(err), port: httpPort, host: httpHost }, `[Overmind] [ERREUR] Échec du démarrage HTTP sur ${httpHost}:${httpPort}.` +
` Port déjà pris ? (EADDRINUSE)`);
process.exit(1);
}
}
else {
try {
await server.start({ transportType: 'stdio' });
rootLogger.info(memoryOnly
? '[Overmind] [READY] Serveur mémoire prêt sur STDIO.'
: '[Overmind] [READY] Serveur prêt sur STDIO.');
}
catch (err) {
rootLogger.error({ error: err instanceof Error ? err.message : String(err) }, '[Overmind] [ERREUR] Échec du démarrage STDIO.');
process.exit(1);
}
}
//# sourceMappingURL=cli.js.map