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
JavaScript
/**
* ╔══════════════════════════════════════════════════════════════════════╗
* ║ 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