UNPKG

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

184 lines • 8.22 kB
/** * ╔══════════════════════════════════════════════════════════════════════╗ * ║ OVERMIND BRIDGE — BridgeHttpClient (JSON-RPC 2.0 HTTP Caller) ║ * ║ ║ * ║ Client HTTP minimaliste pour parler à un OverBridgeServer. ║ * ║ Utilisé par le CLI pour les commandes 'call', 'scenario', etc. ║ * ║ Supporte : ║ * ║ - Single + batch JSON-RPC 2.0 ║ * ║ - Auth Bearer token ║ * ║ - Timeout configurable ║ * ║ - Mode "auto" : démarre un bridge local si pas de serveur ║ * ╚══════════════════════════════════════════════════════════════════════╝ */ import http from 'node:http'; import { URL } from 'node:url'; // ─── BridgeHttpClient ────────────────────────────────────────────────────── export class BridgeHttpClient { config; rpcId = 1; constructor(config) { this.config = { baseUrl: config.baseUrl.replace(/\/+$/, ''), // strip trailing slashes authToken: config.authToken, timeoutMs: config.timeoutMs ?? 60_000, }; } get baseUrl() { return this.config.baseUrl; } // ─── JSON-RPC Calls ────────────────────────────────────────────────────── /** * Appelle une méthode JSON-RPC et retourne le résultat (ou throw). * Retry automatique sur erreurs transitoires (ECONNRESET, ETIMEDOUT, 5xx). */ async call(method, params, timeoutMs) { const req = { jsonrpc: '2.0', id: this.rpcId++, method, params, }; const maxRetries = 2; let lastErr; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const res = await this._post('/rpc', req, timeoutMs ?? this.config.timeoutMs); if (Array.isArray(res)) { throw new Error('Unexpected batch response to single call'); } if (res.error) { const err = new Error(`JSON-RPC error ${res.error.code}: ${res.error.message}`); Object.assign(err, { code: res.error.code, data: res.error.data }); throw err; } return res.result; } catch (e) { lastErr = e; // Ne pas retry sur les erreurs JSON-RPC (codes -32xxx) — ce sont des erreurs applicatives const rpcErr = e; if (rpcErr.code && rpcErr.code >= -32600 && rpcErr.code <= -32000) throw e; // Retry sur erreurs réseau ou 5xx HTTP if (attempt < maxRetries) { await new Promise((r) => setTimeout(r, 500 * (attempt + 1))); // backoff 500ms, 1s continue; } } } throw lastErr; } /** * Batch JSON-RPC (plusieurs calls en un round-trip). */ async callBatch(calls) { const reqs = calls.map((c) => ({ jsonrpc: '2.0', id: this.rpcId++, method: c.method, params: c.params, })); const responses = await this._post('/rpc', reqs, this.config.timeoutMs); if (!Array.isArray(responses)) { throw new Error('Batch call did not return an array'); } return responses.map((r) => { if (r.error) { const err = new Error(`JSON-RPC error ${r.error.code}: ${r.error.message}`); Object.assign(err, { code: r.error.code, data: r.error.data }); throw err; } return r.result; }); } /** * GET /health (pas JSON-RPC, endpoint séparé). */ async health() { const res = await this._get('/health', 5_000); return JSON.parse(res); } // ─── HTTP Helpers ──────────────────────────────────────────────────────── async _post(path, body, timeoutMs) { const url = new URL(this.config.baseUrl + path); const data = JSON.stringify(body); return new Promise((resolve, reject) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); const req = http.request({ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'Content-Length': Buffer.byteLength(data), ...(this.config.authToken ? { Authorization: `Bearer ${this.config.authToken}` } : {}), }, signal: controller.signal, }, (res) => { clearTimeout(timer); const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf-8'); if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(text)); } catch (_err) { reject(new Error(`Invalid JSON response: ${text.slice(0, 200)}`)); } } else { reject(new Error(`HTTP ${res.statusCode}: ${text.slice(0, 200)}`)); } }); res.on('error', reject); }); req.on('error', (err) => { clearTimeout(timer); reject(err); }); req.write(data); req.end(); }); } async _get(path, timeoutMs) { const url = new URL(this.config.baseUrl + path); return new Promise((resolve, reject) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); const req = http.request({ hostname: url.hostname, port: url.port || 80, path: url.pathname + url.search, method: 'GET', headers: { Accept: 'application/json' }, signal: controller.signal, }, (res) => { clearTimeout(timer); const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf-8'); if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { resolve(text); } else { reject(new Error(`HTTP ${res.statusCode}: ${text.slice(0, 200)}`)); } }); res.on('error', reject); }); req.on('error', (err) => { clearTimeout(timer); reject(err); }); req.end(); }); } } //# sourceMappingURL=BridgeHttpClient.js.map