From 0bb8af199efb34bec8d697e599f61b475ae74d71 Mon Sep 17 00:00:00 2001 From: StillHammer Date: Sat, 24 Jan 2026 04:05:01 +0700 Subject: [PATCH] =?UTF-8?q?v2.0=20-=20Architecture=20unifi=C3=A9e=20avec?= =?UTF-8?q?=20SQLite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MCP unifié : mcp-partner remplace mcp-master et mcp-slave - Messages bufferisés : SQLite stocke tout, pas besoin d'être connecté en permanence - Tools simplifiés : register, talk, check_messages, listen, reply, list_partners, history - Suppression du hook Stop (plus nécessaire avec reply explicite) - Heartbeat 30s pour éviter les déconnexions idle - ID basé sur le nom du dossier (unique par projet) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 2 + README.md | 194 +++++++---------- broker/db.js | 181 ++++++++++++++++ broker/index.js | 338 +++++++++++------------------- hooks/on-stop.js | 109 ---------- mcp-master/index.js | 194 ----------------- mcp-partner/index.js | 482 +++++++++++++++++++++++++++++++++++++++++++ mcp-slave/index.js | 282 ------------------------- package-lock.json | 412 +++++++++++++++++++++++++++++++++++- package.json | 5 +- 10 files changed, 1273 insertions(+), 926 deletions(-) create mode 100644 broker/db.js delete mode 100644 hooks/on-stop.js delete mode 100644 mcp-master/index.js create mode 100644 mcp-partner/index.js delete mode 100644 mcp-slave/index.js diff --git a/.gitignore b/.gitignore index 9f237d9..6d7449c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ .state/ conversations/ +data/ *.log +*.db diff --git a/README.md b/README.md index 83c9ced..b68bf20 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ # MCP Claude Duo -Fait discuter deux instances Claude Code ensemble via MCP. +MCP pour faire discuter plusieurs instances Claude Code ensemble. -## Architecture +## Architecture v2 ``` -Terminal 1 (Master) Terminal 2 (Slave) -┌─────────────────┐ ┌─────────────────┐ -│ Claude Code │ │ Claude Code │ -│ + MCP Master │ ┌───────┐ │ + MCP Slave │ -│ │ │Broker │ │ + Hook Stop │ -│ talk("yo") ────┼────────►│ HTTP │─────────►│ │ -│ │ │ │ │ reçoit "yo" │ -│ │ │ │ │ répond "salut" │ -│ reçoit "salut"◄┼─────────│ │◄─────────┼── (hook envoie) │ -└─────────────────┘ └───────┘ └─────────────────┘ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Claude A │ │ Broker │ │ Claude B │ +│ (projet-a) │◄───►│ HTTP + SQLite │◄───►│ (projet-b) │ +│ + mcp-partner │ │ │ │ + mcp-partner │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ ``` +- **Un seul MCP unifié** : `mcp-partner` pour tout le monde +- **Messages bufferisés** : SQLite stocke les messages, pas besoin d'être connecté en permanence +- **Bidirectionnel** : tout le monde peut parler à tout le monde + ## Installation ```bash @@ -26,130 +25,93 @@ npm install ## Démarrage -### 1. Lancer le broker (dans un terminal séparé) +### 1. Lancer le broker ```bash npm run broker ``` -Le broker tourne sur `http://localhost:3210` par défaut. +Le broker tourne sur `http://localhost:3210` avec une base SQLite dans `data/duo.db`. -### 2. Configurer le Master (Terminal 1) +### 2. Configurer le MCP (global) -Ajoute dans ta config MCP Claude Code (`~/.claude.json` ou settings): - -```json -{ - "mcpServers": { - "duo-master": { - "command": "node", - "args": ["C:/Users/alexi/Documents/projects/mcp-claude-duo/mcp-master/index.js"], - "env": { - "BROKER_URL": "http://localhost:3210" - } - } - } -} +```bash +claude mcp add duo-partner -s user -e BROKER_URL=http://localhost:3210 -- node "CHEMIN/mcp-claude-duo/mcp-partner/index.js" ``` -### 3. Configurer le Slave (Terminal 2) - -Config MCP: - -```json -{ - "mcpServers": { - "duo-slave": { - "command": "node", - "args": ["C:/Users/alexi/Documents/projects/mcp-claude-duo/mcp-slave/index.js"], - "env": { - "BROKER_URL": "http://localhost:3210", - "SLAVE_NAME": "Bob" - } - } - } -} -``` - -Config Hook (dans `.claude/settings.json` du projet ou `~/.claude/settings.json`): - -```json -{ - "hooks": { - "Stop": [ - { - "matcher": {}, - "hooks": [ - { - "type": "command", - "command": "node C:/Users/alexi/Documents/projects/mcp-claude-duo/hooks/on-stop.js" - } - ] - } - ] - } -} -``` - -## Utilisation - -### Terminal 2 (Slave) - Se connecter - -``` -Toi: "Connecte-toi en tant que partenaire" -Claude: *utilise connect()* → "Connecté, en attente de messages..." -``` - -### Terminal 1 (Master) - Parler - -``` -Toi: "Parle à mon partenaire, dis lui salut" -Claude: *utilise talk("Salut !")* -→ attend la réponse... -→ "Partenaire: Hey ! Ça va ?" -``` - -### Terminal 2 (Slave) - Reçoit et répond - -``` -Claude: "Message reçu: Salut !" -Claude: "Je lui réponds..." → écrit sa réponse -→ Le hook capture et envoie au broker -Claude: *utilise connect()* → attend le prochain message +Ou par projet : +```bash +cd mon-projet +claude mcp add duo-partner -s project -e BROKER_URL=http://localhost:3210 -e PARTNER_NAME="Mon Nom" -- node "CHEMIN/mcp-claude-duo/mcp-partner/index.js" ``` ## Tools disponibles -### MCP Master | Tool | Description | |------|-------------| -| `talk(message)` | Envoie un message et attend la réponse | -| `list_partners()` | Liste les partenaires connectés | +| `register(name?)` | S'enregistrer sur le réseau | +| `talk(message, to?)` | Envoyer un message et attendre la réponse | +| `check_messages(wait?)` | Vérifier les messages en attente | +| `listen()` | Écouter en temps réel (long-polling) | +| `reply(message)` | Répondre au dernier message reçu | +| `list_partners()` | Lister les partenaires connectés | +| `history(partnerId, limit?)` | Historique de conversation | -### MCP Slave -| Tool | Description | -|------|-------------| -| `connect(name?)` | Se connecte et attend les messages | -| `disconnect()` | Se déconnecte | +## Exemples -## Variables d'environnement +### Conversation simple -| Variable | Description | Défaut | -|----------|-------------|--------| -| `BROKER_URL` | URL du broker | `http://localhost:3210` | -| `BROKER_PORT` | Port du broker | `3210` | -| `SLAVE_NAME` | Nom du slave | `Partner` | +**Claude A :** +``` +register("Alice") +talk("Salut, ça va ?") +→ attend la réponse... +→ "Bob: Oui et toi ?" +``` -## Flow détaillé +**Claude B :** +``` +register("Bob") +listen() +→ "Alice: Salut, ça va ?" +reply("Oui et toi ?") +``` -1. **Slave** appelle `connect()` → s'enregistre au broker, attend (long-polling) -2. **Master** appelle `talk("message")` → envoie au broker -3. **Broker** transmet au slave → `connect()` retourne le message -4. **Slave** Claude voit le message et répond naturellement -5. **Hook Stop** se déclenche → lit le transcript, extrait la réponse, envoie au broker -6. **Broker** retourne la réponse au master -7. **Master** `talk()` retourne avec la réponse -8. **Slave** rappelle `connect()` pour le prochain message +### Messages bufferisés + +**Claude A envoie même si B n'est pas connecté :** +``` +talk("Hey, t'es là ?") +→ message stocké en DB, attend la réponse... +``` + +**Claude B se connecte plus tard :** +``` +check_messages() +→ "Alice: Hey, t'es là ?" +reply("Oui, j'arrive !") +→ Claude A reçoit la réponse +``` + +## API Broker + +| Endpoint | Description | +|----------|-------------| +| `POST /register` | S'enregistrer | +| `POST /talk` | Envoyer et attendre réponse | +| `GET /messages/:id` | Récupérer messages non lus | +| `GET /wait/:id` | Long-polling | +| `POST /respond` | Répondre à un message | +| `GET /partners` | Lister les partenaires | +| `GET /history/:a/:b` | Historique entre deux partenaires | +| `GET /health` | Status du broker | + +## Base de données + +SQLite dans `data/duo.db` : + +- `partners` : ID, nom, status, dernière connexion +- `messages` : contenu, expéditeur, destinataire, timestamps ## License diff --git a/broker/db.js b/broker/db.js new file mode 100644 index 0000000..76f2cd5 --- /dev/null +++ b/broker/db.js @@ -0,0 +1,181 @@ +import Database from "better-sqlite3"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { mkdirSync } from "fs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const dataDir = join(__dirname, "..", "data"); + +// Créer le dossier data +try { + mkdirSync(dataDir, { recursive: true }); +} catch {} + +const dbPath = join(dataDir, "duo.db"); +const db = new Database(dbPath); + +// Activer les foreign keys +db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON"); + +// Créer les tables +db.exec(` + -- Partenaires + CREATE TABLE IF NOT EXISTS partners ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'online' + ); + + -- Messages + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_id TEXT NOT NULL, + to_id TEXT NOT NULL, + content TEXT NOT NULL, + request_id TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + delivered_at DATETIME, + response_to INTEGER REFERENCES messages(id), + FOREIGN KEY (from_id) REFERENCES partners(id), + FOREIGN KEY (to_id) REFERENCES partners(id) + ); + + -- Index pour les requêtes fréquentes + CREATE INDEX IF NOT EXISTS idx_messages_to_id ON messages(to_id, delivered_at); + CREATE INDEX IF NOT EXISTS idx_messages_request_id ON messages(request_id); +`); + +// Prepared statements +const stmts = { + // Partners + upsertPartner: db.prepare(` + INSERT INTO partners (id, name, last_seen, status) + VALUES (?, ?, CURRENT_TIMESTAMP, 'online') + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + last_seen = CURRENT_TIMESTAMP, + status = 'online' + `), + + getPartner: db.prepare(`SELECT * FROM partners WHERE id = ?`), + + getAllPartners: db.prepare(`SELECT * FROM partners ORDER BY last_seen DESC`), + + updatePartnerStatus: db.prepare(` + UPDATE partners SET status = ?, last_seen = CURRENT_TIMESTAMP WHERE id = ? + `), + + // Messages + insertMessage: db.prepare(` + INSERT INTO messages (from_id, to_id, content, request_id) + VALUES (?, ?, ?, ?) + `), + + getUndeliveredMessages: db.prepare(` + SELECT * FROM messages + WHERE to_id = ? AND delivered_at IS NULL + ORDER BY created_at ASC + `), + + markDelivered: db.prepare(` + UPDATE messages SET delivered_at = CURRENT_TIMESTAMP WHERE id = ? + `), + + getMessageByRequestId: db.prepare(` + SELECT * FROM messages WHERE request_id = ? + `), + + insertResponse: db.prepare(` + INSERT INTO messages (from_id, to_id, content, response_to) + VALUES (?, ?, ?, ?) + `), + + getResponse: db.prepare(` + SELECT * FROM messages WHERE response_to = ? AND delivered_at IS NULL + `), + + markResponseDelivered: db.prepare(` + UPDATE messages SET delivered_at = CURRENT_TIMESTAMP WHERE response_to = ? + `), + + // Conversations history + getConversation: db.prepare(` + SELECT * FROM messages + WHERE (from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?) + ORDER BY created_at DESC + LIMIT ? + `), +}; + +// API +export const DB = { + // Partners + registerPartner(id, name) { + stmts.upsertPartner.run(id, name); + return stmts.getPartner.get(id); + }, + + getPartner(id) { + return stmts.getPartner.get(id); + }, + + getAllPartners() { + return stmts.getAllPartners.all(); + }, + + setPartnerOffline(id) { + stmts.updatePartnerStatus.run("offline", id); + }, + + setPartnerOnline(id) { + stmts.updatePartnerStatus.run("online", id); + }, + + // Messages + sendMessage(fromId, toId, content, requestId = null) { + const result = stmts.insertMessage.run(fromId, toId, content, requestId); + return result.lastInsertRowid; + }, + + getUndeliveredMessages(toId) { + return stmts.getUndeliveredMessages.all(toId); + }, + + markDelivered(messageId) { + stmts.markDelivered.run(messageId); + }, + + // Pour talk() qui attend une réponse + sendAndWaitResponse(fromId, toId, content, requestId) { + stmts.insertMessage.run(fromId, toId, content, requestId); + }, + + getMessageByRequestId(requestId) { + return stmts.getMessageByRequestId.get(requestId); + }, + + sendResponse(fromId, toId, content, originalMessageId) { + stmts.insertResponse.run(fromId, toId, content, originalMessageId); + }, + + getResponse(originalMessageId) { + return stmts.getResponse.get(originalMessageId); + }, + + markResponseDelivered(originalMessageId) { + stmts.markResponseDelivered.run(originalMessageId); + }, + + // History + getConversation(partnerId1, partnerId2, limit = 50) { + return stmts.getConversation.all(partnerId1, partnerId2, partnerId2, partnerId1, limit); + }, + + // Raw access for complex queries + raw: db, +}; + +export default DB; diff --git a/broker/index.js b/broker/index.js index bcb50f4..76377e3 100644 --- a/broker/index.js +++ b/broker/index.js @@ -1,76 +1,22 @@ import express from "express"; -import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; +import { DB } from "./db.js"; const app = express(); app.use(express.json()); const PORT = process.env.BROKER_PORT || 3210; -// Dossier pour sauvegarder les conversations -const __dirname = dirname(fileURLToPath(import.meta.url)); -const conversationsDir = join(__dirname, "..", "conversations"); -try { - mkdirSync(conversationsDir, { recursive: true }); -} catch {} - -// Conversations actives: { visitorId: { date, messages: [] } } -const activeConversations = new Map(); - -/** - * Génère l'ID de conversation basé sur les deux partners et la date - */ -function getConversationId(partnerId) { - const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD - return `${partnerId}_${today}`; -} - -/** - * Sauvegarde un message dans la conversation - */ -function saveMessage(partnerId, from, content) { - const convId = getConversationId(partnerId); - const convFile = join(conversationsDir, `${convId}.json`); - - let conversation; - if (existsSync(convFile)) { - conversation = JSON.parse(readFileSync(convFile, "utf-8")); - } else { - conversation = { - id: convId, - partnerId, - startedAt: new Date().toISOString(), - messages: [], - }; - } - - conversation.messages.push({ - from, - content, - timestamp: new Date().toISOString(), - }); - - writeFileSync(convFile, JSON.stringify(conversation, null, 2)); - return conversation; -} - -// Slaves connectés: { id: { name, connectedAt, waitingResponse } } -const partners = new Map(); - -// Messages en attente pour chaque slave: { partnerId: [{ from, content, timestamp }] } -const pendingMessages = new Map(); - -// Réponses en attente pour le master: { requestId: { resolve, timeout } } +// Réponses en attente (pour talk qui attend une réponse) +// { requestId: { resolve, fromId, toId } } const pendingResponses = new Map(); -// Long-polling requests en attente: { partnerId: { res, timeout } } +// Long-polling en attente (pour check_messages) +// { partnerId: { res, heartbeat } } const waitingPartners = new Map(); /** - * Slave s'enregistre + * S'enregistrer * POST /register - * Body: { partnerId, name } */ app.post("/register", (req, res) => { const { partnerId, name } = req.body; @@ -79,46 +25,88 @@ app.post("/register", (req, res) => { return res.status(400).json({ error: "partnerId required" }); } - partners.set(partnerId, { - name: name || partnerId, - connectedAt: Date.now(), - lastSeen: Date.now(), - status: "connected", - }); + const partner = DB.registerPartner(partnerId, name || partnerId); + console.log(`[BROKER] Registered: ${partner.name} (${partnerId})`); - pendingMessages.set(partnerId, []); - - console.log(`[BROKER] Slave registered: ${name || partnerId} (${partnerId})`); - - res.json({ success: true, message: "Registered" }); + res.json({ success: true, partner }); }); /** - * Slave attend un message (long-polling) + * Envoyer un message et attendre la réponse + * POST /talk + */ +app.post("/talk", (req, res) => { + const { fromId, toId, content } = req.body; + + if (!fromId || !toId || !content) { + return res.status(400).json({ error: "fromId, toId, and content required" }); + } + + const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Enregistrer le message en DB + const messageId = DB.sendMessage(fromId, toId, content, requestId); + + console.log(`[BROKER] ${fromId} -> ${toId}: "${content.substring(0, 50)}..."`); + + // Notifier le destinataire s'il est en attente + notifyWaitingPartner(toId); + + // Attendre la réponse (pas de timeout) + const responsePromise = new Promise((resolve) => { + pendingResponses.set(requestId, { resolve, fromId, toId, messageId }); + }); + + responsePromise.then((response) => { + res.json(response); + }); +}); + +/** + * Récupérer les messages non lus + * GET /messages/:partnerId + */ +app.get("/messages/:partnerId", (req, res) => { + const { partnerId } = req.params; + + const messages = DB.getUndeliveredMessages(partnerId); + + // Marquer comme délivrés + for (const msg of messages) { + DB.markDelivered(msg.id); + } + + res.json({ messages }); +}); + +/** + * Attendre des messages (long-polling) * GET /wait/:partnerId */ app.get("/wait/:partnerId", (req, res) => { const { partnerId } = req.params; - if (!partners.has(partnerId)) { - return res.status(404).json({ error: "Slave not registered" }); - } + // Mettre à jour le status + DB.setPartnerOnline(partnerId); - // Check s'il y a déjà un message en attente - const messages = pendingMessages.get(partnerId) || []; + // Check s'il y a des messages en attente + const messages = DB.getUndeliveredMessages(partnerId); if (messages.length > 0) { - const msg = messages.shift(); - return res.json({ hasMessage: true, message: msg }); + // Marquer comme délivrés + for (const msg of messages) { + DB.markDelivered(msg.id); + } + return res.json({ hasMessages: true, messages }); } // Annuler l'ancien waiting s'il existe if (waitingPartners.has(partnerId)) { const old = waitingPartners.get(partnerId); if (old.heartbeat) clearInterval(old.heartbeat); - old.res.json({ hasMessage: false, message: null, reason: "reconnect" }); + old.res.json({ hasMessages: false, messages: [], reason: "reconnect" }); } - // Heartbeat toutes les 30 secondes pour garder la connexion vivante (bug Claude Code) + // Heartbeat toutes les 30s const heartbeat = setInterval(() => { try { res.write(": heartbeat\n\n"); @@ -131,181 +119,89 @@ app.get("/wait/:partnerId", (req, res) => { res.on("close", () => { clearInterval(heartbeat); waitingPartners.delete(partnerId); - // Marquer le partner comme déconnecté (mais garder dans la liste pour permettre reconnexion) - if (partners.has(partnerId)) { - const info = partners.get(partnerId); - info.lastSeen = Date.now(); - info.status = "disconnected"; - } - console.log(`[BROKER] Connection closed for ${partnerId}`); + DB.setPartnerOffline(partnerId); + console.log(`[BROKER] ${partnerId} disconnected`); }); waitingPartners.set(partnerId, { res, heartbeat }); - - // Mettre à jour le status - if (partners.has(partnerId)) { - const info = partners.get(partnerId); - info.lastSeen = Date.now(); - info.status = "waiting"; - } }); /** - * Master envoie un message à un slave - * POST /send - * Body: { partnerId, content, requestId } - */ -app.post("/send", (req, res) => { - const { partnerId, content, requestId } = req.body; - - if (!partnerId || !content) { - return res.status(400).json({ error: "partnerId and content required" }); - } - - if (!partners.has(partnerId)) { - return res.status(404).json({ error: "Slave not found" }); - } - - const message = { - content, - requestId, - timestamp: Date.now(), - }; - - console.log(`[BROKER] Master -> ${partnerId}: "${content.substring(0, 50)}..."`); - - // Sauvegarder le message du master - saveMessage(partnerId, "master", content); - - // Si le slave est en attente (long-polling), lui envoyer directement - if (waitingPartners.has(partnerId)) { - const { res: partnerRes, heartbeat } = waitingPartners.get(partnerId); - if (heartbeat) clearInterval(heartbeat); - waitingPartners.delete(partnerId); - partnerRes.json({ hasMessage: true, message }); - } else { - // Sinon, mettre en queue - const messages = pendingMessages.get(partnerId) || []; - messages.push(message); - pendingMessages.set(partnerId, messages); - } - - // Attendre la réponse du slave (pas de timeout) - const responsePromise = new Promise((resolve) => { - pendingResponses.set(requestId, { resolve, timeout: null }); - }); - - responsePromise.then((response) => { - res.json(response); - }); -}); - -/** - * Slave envoie sa réponse (appelé par le hook Stop) + * Répondre à un message * POST /respond - * Body: { partnerId, requestId, content } */ app.post("/respond", (req, res) => { - const { partnerId, requestId, content } = req.body; + const { fromId, toId, content, requestId } = req.body; - console.log(`[BROKER] ${partnerId} responded: "${content.substring(0, 50)}..."`); - - // Sauvegarder la réponse du partner - if (partnerId && content) { - saveMessage(partnerId, partnerId, content); - } + console.log(`[BROKER] ${fromId} responded to ${toId}: "${content.substring(0, 50)}..."`); + // Trouver la requête en attente if (requestId && pendingResponses.has(requestId)) { - const { resolve, timeout } = pendingResponses.get(requestId); - clearTimeout(timeout); + const { resolve, messageId } = pendingResponses.get(requestId); pendingResponses.delete(requestId); + + // Enregistrer la réponse en DB + DB.sendResponse(fromId, toId, content, messageId); + resolve({ success: true, content }); + } else { + // Pas de requête en attente, juste enregistrer comme message normal + DB.sendMessage(fromId, toId, content, null); + notifyWaitingPartner(toId); } res.json({ success: true }); }); /** - * Liste les partners connectés + * Liste les partenaires * GET /partners */ app.get("/partners", (req, res) => { - const list = []; - for (const [id, info] of partners) { - list.push({ id, ...info }); - } - res.json({ partners: list }); + const partners = DB.getAllPartners(); + res.json({ partners }); }); /** - * Slave se déconnecte - * POST /disconnect + * Historique de conversation + * GET /history/:partner1/:partner2 */ -app.post("/disconnect", (req, res) => { - const { partnerId } = req.body; +app.get("/history/:partner1/:partner2", (req, res) => { + const { partner1, partner2 } = req.params; + const limit = parseInt(req.query.limit) || 50; - if (partners.has(partnerId)) { - partners.delete(partnerId); - pendingMessages.delete(partnerId); - - if (waitingPartners.has(partnerId)) { - const { res: partnerRes, timeout } = waitingPartners.get(partnerId); - if (timeout) clearTimeout(timeout); - partnerRes.json({ hasMessage: false, disconnected: true }); - waitingPartners.delete(partnerId); - } - - console.log(`[BROKER] Slave disconnected: ${partnerId}`); - } - - res.json({ success: true }); -}); - -/** - * Liste les conversations sauvegardées - * GET /conversations - */ -app.get("/conversations", (req, res) => { - try { - const files = readdirSync(conversationsDir).filter((f) => f.endsWith(".json")); - const conversations = files.map((f) => { - const conv = JSON.parse(readFileSync(join(conversationsDir, f), "utf-8")); - return { - id: conv.id, - partnerId: conv.partnerId, - startedAt: conv.startedAt, - messageCount: conv.messages.length, - }; - }); - res.json({ conversations }); - } catch (error) { - res.json({ conversations: [] }); - } -}); - -/** - * Récupère une conversation spécifique - * GET /conversations/:id - */ -app.get("/conversations/:id", (req, res) => { - const { id } = req.params; - const convFile = join(conversationsDir, `${id}.json`); - - if (!existsSync(convFile)) { - return res.status(404).json({ error: "Conversation not found" }); - } - - const conversation = JSON.parse(readFileSync(convFile, "utf-8")); - res.json(conversation); + const messages = DB.getConversation(partner1, partner2, limit); + res.json({ messages }); }); /** * Health check */ app.get("/health", (req, res) => { - res.json({ status: "ok", partners: partners.size }); + const partners = DB.getAllPartners(); + const online = partners.filter((p) => p.status === "online").length; + res.json({ status: "ok", partners: partners.length, online }); }); +/** + * Notifie un partenaire en attente qu'il a des messages + */ +function notifyWaitingPartner(partnerId) { + if (waitingPartners.has(partnerId)) { + const { res, heartbeat } = waitingPartners.get(partnerId); + clearInterval(heartbeat); + waitingPartners.delete(partnerId); + + const messages = DB.getUndeliveredMessages(partnerId); + for (const msg of messages) { + DB.markDelivered(msg.id); + } + + res.json({ hasMessages: true, messages }); + } +} + app.listen(PORT, () => { - console.log(`[BROKER] Claude Duo Broker running on http://localhost:${PORT}`); + console.log(`[BROKER] Claude Duo Broker v2 running on http://localhost:${PORT}`); + console.log(`[BROKER] Database: data/duo.db`); }); diff --git a/hooks/on-stop.js b/hooks/on-stop.js deleted file mode 100644 index d237c35..0000000 --- a/hooks/on-stop.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node - -/** - * Hook "Stop" pour Claude Code - * Se déclenche quand Claude finit de répondre - * Lit le transcript et envoie la dernière réponse au broker - */ - -import { readFileSync, readdirSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; - -const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const stateDir = join(__dirname, "..", ".state"); - -async function main() { - // Lire l'input du hook depuis stdin - let input = ""; - for await (const chunk of process.stdin) { - input += chunk; - } - - let hookData; - try { - hookData = JSON.parse(input); - } catch { - // Pas de données valides - process.exit(0); - } - - const { transcript_path } = hookData; - - if (!transcript_path) { - process.exit(0); - } - - // Lire le fichier state unique - const stateFile = join(stateDir, "current.json"); - let state = null; - try { - const content = readFileSync(stateFile, "utf-8"); - state = JSON.parse(content); - } catch { - // Pas de state, pas de partner connecté - process.exit(0); - } - - if (!state || !state.currentRequestId) { - process.exit(0); - } - - // Lire le transcript - let transcript; - try { - transcript = JSON.parse(readFileSync(transcript_path, "utf-8")); - } catch { - process.exit(0); - } - - // Trouver la dernière réponse de l'assistant - let lastAssistantMessage = null; - - // Le transcript peut avoir différents formats, essayons de trouver les messages - const messages = transcript.messages || transcript; - - if (Array.isArray(messages)) { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant" && msg.content) { - // Extraire le texte du contenu - if (typeof msg.content === "string") { - lastAssistantMessage = msg.content; - } else if (Array.isArray(msg.content)) { - // Chercher les blocs de texte - const textBlocks = msg.content.filter((c) => c.type === "text"); - if (textBlocks.length > 0) { - lastAssistantMessage = textBlocks.map((t) => t.text).join("\n"); - } - } - break; - } - } - } - - if (!lastAssistantMessage) { - process.exit(0); - } - - // Envoyer la réponse au broker - try { - await fetch(`${BROKER_URL}/respond`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - partnerId: state.partnerId, - requestId: state.currentRequestId, - content: lastAssistantMessage, - }), - }); - } catch (error) { - console.error(`[HOOK] Error sending response: ${error.message}`); - } - - process.exit(0); -} - -main(); diff --git a/mcp-master/index.js b/mcp-master/index.js deleted file mode 100644 index 69b2f40..0000000 --- a/mcp-master/index.js +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210"; - -/** - * Appel HTTP au broker - */ -async function brokerFetch(path, options = {}) { - const url = `${BROKER_URL}${path}`; - const response = await fetch(url, { - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); - return response.json(); -} - -// Créer le serveur MCP -const server = new Server( - { - name: "mcp-claude-duo-master", - version: "1.0.0", - }, - { - capabilities: { - tools: {}, - }, - } -); - -// Liste des tools -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "talk", - description: - "Envoie un message à ton partenaire Claude et attend sa réponse. Utilise ceci pour avoir une conversation.", - inputSchema: { - type: "object", - properties: { - message: { - type: "string", - description: "Le message à envoyer à ton partenaire", - }, - partnerId: { - type: "string", - description: - "L'ID du partenaire (optionnel si un seul partenaire connecté)", - }, - }, - required: ["message"], - }, - }, - { - name: "list_partners", - description: "Liste tous les partenaires Claude connectés et disponibles", - inputSchema: { - type: "object", - properties: {}, - }, - }, - ], - }; -}); - -// Handler des tools -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - switch (name) { - case "talk": { - try { - // Récupérer la liste des slaves si pas de partnerId spécifié - let partnerId = args.partnerId; - - if (!partnerId) { - const { partners } = await brokerFetch("/partners"); - if (!partners || partners.length === 0) { - return { - content: [ - { - type: "text", - text: "Aucun partenaire connecté. Demande à ton partenaire de se connecter d'abord.", - }, - ], - }; - } - partnerId = partners[0].id; - } - - const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - const response = await brokerFetch("/send", { - method: "POST", - body: JSON.stringify({ - partnerId, - content: args.message, - requestId, - }), - }); - - if (response.error) { - return { - content: [{ type: "text", text: `Erreur: ${response.error}` }], - isError: true, - }; - } - - return { - content: [ - { - type: "text", - text: `**Partenaire:** ${response.content}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Erreur de communication avec le broker: ${error.message}`, - }, - ], - isError: true, - }; - } - } - - case "list_partners": { - try { - const { partners } = await brokerFetch("/partners"); - - if (!partners || partners.length === 0) { - return { - content: [ - { - type: "text", - text: "Aucun partenaire connecté pour le moment.", - }, - ], - }; - } - - let text = "**Partenaires connectés:**\n\n"; - for (const partner of partners) { - const connectedSince = Math.round( - (Date.now() - partner.connectedAt) / 1000 - ); - text += `- **${partner.name}** (ID: ${partner.id}) - connecté depuis ${connectedSince}s\n`; - } - - return { - content: [{ type: "text", text }], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Erreur: ${error.message}. Le broker est-il lancé ?`, - }, - ], - isError: true, - }; - } - } - - default: - return { - content: [{ type: "text", text: `Tool inconnu: ${name}` }], - isError: true, - }; - } -}); - -// Démarrer -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("[MCP-MASTER] Started"); -} - -main().catch(console.error); diff --git a/mcp-partner/index.js b/mcp-partner/index.js new file mode 100644 index 0000000..e0afff3 --- /dev/null +++ b/mcp-partner/index.js @@ -0,0 +1,482 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210"; +const PARTNER_NAME = process.env.PARTNER_NAME || "Claude"; + +// ID basé sur le dossier de travail (unique par projet) +const cwd = process.cwd(); +const projectName = cwd.split(/[/\\]/).pop().toLowerCase().replace(/[^a-z0-9]/g, "_"); +const myId = projectName || "partner"; + +let isRegistered = false; +let lastReceivedRequestId = null; // Pour savoir à quel message répondre + +/** + * Appel HTTP au broker + */ +async function brokerFetch(path, options = {}, timeoutMs = 0) { + const url = `${BROKER_URL}${path}`; + + const fetchOptions = { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }; + + if (timeoutMs > 0) { + const controller = new AbortController(); + setTimeout(() => controller.abort(), timeoutMs); + fetchOptions.signal = controller.signal; + } + + const response = await fetch(url, fetchOptions); + return response.json(); +} + +/** + * S'enregistrer auprès du broker + */ +async function ensureRegistered() { + if (!isRegistered) { + await brokerFetch("/register", { + method: "POST", + body: JSON.stringify({ partnerId: myId, name: PARTNER_NAME }), + }); + isRegistered = true; + console.error(`[MCP-PARTNER] Registered as ${PARTNER_NAME} (${myId})`); + } +} + +// Créer le serveur MCP +const server = new Server( + { + name: "mcp-claude-duo-partner", + version: "2.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +// Liste des tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "register", + description: + "S'enregistre auprès du réseau de conversation. Utilise au début pour te connecter.", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "Ton nom/pseudo (optionnel)", + }, + }, + }, + }, + { + name: "talk", + description: + "Envoie un message à un partenaire et attend sa réponse. Pour initier ou continuer une conversation.", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "Le message à envoyer", + }, + to: { + type: "string", + description: "L'ID du destinataire (optionnel si un seul partenaire)", + }, + }, + required: ["message"], + }, + }, + { + name: "check_messages", + description: + "Vérifie s'il y a des messages en attente. Les messages sont bufferisés, donc pas besoin d'écouter en permanence.", + inputSchema: { + type: "object", + properties: { + wait: { + type: "boolean", + description: "Si true, attend qu'un message arrive (long-polling). Sinon retourne immédiatement.", + }, + }, + }, + }, + { + name: "reply", + description: + "Répond au dernier message reçu. À utiliser après check_messages quand quelqu'un attend ta réponse.", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "Ta réponse", + }, + }, + required: ["message"], + }, + }, + { + name: "listen", + description: + "Écoute en temps réel les messages entrants (long-polling). Bloque jusqu'à ce qu'un message arrive.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "list_partners", + description: "Liste tous les partenaires connectés au réseau.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "history", + description: "Récupère l'historique de conversation avec un partenaire.", + inputSchema: { + type: "object", + properties: { + partnerId: { + type: "string", + description: "L'ID du partenaire", + }, + limit: { + type: "number", + description: "Nombre de messages max (défaut: 20)", + }, + }, + required: ["partnerId"], + }, + }, + ], + }; +}); + +// Handler des tools +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case "register": { + try { + const displayName = args.name || PARTNER_NAME; + await brokerFetch("/register", { + method: "POST", + body: JSON.stringify({ partnerId: myId, name: displayName }), + }); + isRegistered = true; + + return { + content: [ + { + type: "text", + text: `Connecté en tant que **${displayName}** (ID: ${myId})`, + }, + ], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "talk": { + try { + await ensureRegistered(); + + // Trouver le destinataire + let toId = args.to; + if (!toId) { + const { partners } = await brokerFetch("/partners"); + const other = partners?.find((p) => p.id !== myId); + if (!other) { + return { + content: [ + { + type: "text", + text: "Aucun partenaire connecté. Attends qu'un autre Claude se connecte.", + }, + ], + }; + } + toId = other.id; + } + + const response = await brokerFetch("/talk", { + method: "POST", + body: JSON.stringify({ + fromId: myId, + toId, + content: args.message, + }), + }); + + if (response.error) { + return { + content: [{ type: "text", text: `Erreur: ${response.error}` }], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: `**${toId}:** ${response.content}`, + }, + ], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "check_messages": { + try { + await ensureRegistered(); + + let response; + if (args.wait) { + // Long-polling + response = await brokerFetch(`/wait/${myId}`); + } else { + // Récupération immédiate + response = await brokerFetch(`/messages/${myId}`); + response = { messages: response.messages, hasMessages: response.messages?.length > 0 }; + } + + if (!response.hasMessages || !response.messages?.length) { + return { + content: [ + { + type: "text", + text: "Pas de nouveaux messages.", + }, + ], + }; + } + + // Formater les messages + let text = `**${response.messages.length} message(s) reçu(s):**\n\n`; + for (const msg of response.messages) { + text += `**${msg.from_id}:** ${msg.content}\n`; + // Garder le request_id du dernier message pour pouvoir y répondre + if (msg.request_id) { + lastReceivedRequestId = msg.request_id; + } + } + + if (lastReceivedRequestId) { + text += `\n_Utilise \`reply\` pour répondre._`; + } + + return { + content: [{ type: "text", text }], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "reply": { + try { + await ensureRegistered(); + + if (!lastReceivedRequestId) { + return { + content: [ + { + type: "text", + text: "Aucun message en attente de réponse. Utilise `check_messages` d'abord.", + }, + ], + }; + } + + // Trouver le destinataire original + const { partners } = await brokerFetch("/partners"); + const other = partners?.find((p) => p.id !== myId); + const toId = other?.id || "unknown"; + + await brokerFetch("/respond", { + method: "POST", + body: JSON.stringify({ + fromId: myId, + toId, + content: args.message, + requestId: lastReceivedRequestId, + }), + }); + + lastReceivedRequestId = null; + + return { + content: [ + { + type: "text", + text: "Réponse envoyée.", + }, + ], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "listen": { + try { + await ensureRegistered(); + + // Long-polling - attend qu'un message arrive + console.error("[MCP-PARTNER] Listening..."); + const response = await brokerFetch(`/wait/${myId}`); + + if (!response.hasMessages || !response.messages?.length) { + return { + content: [ + { + type: "text", + text: "Timeout. Rappelle `listen` pour continuer à écouter.", + }, + ], + }; + } + + // Formater les messages + let text = ""; + for (const msg of response.messages) { + text += `**${msg.from_id}:** ${msg.content}\n`; + if (msg.request_id) { + lastReceivedRequestId = msg.request_id; + } + } + + if (lastReceivedRequestId) { + text += `\n_Utilise \`reply\` pour répondre._`; + } + + return { + content: [{ type: "text", text }], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "list_partners": { + try { + const { partners } = await brokerFetch("/partners"); + + if (!partners?.length) { + return { + content: [{ type: "text", text: "Aucun partenaire enregistré." }], + }; + } + + let text = "**Partenaires:**\n\n"; + for (const p of partners) { + const status = p.status === "online" ? "🟢" : "⚫"; + const isMe = p.id === myId ? " (toi)" : ""; + text += `${status} **${p.name}** (${p.id})${isMe}\n`; + } + + return { + content: [{ type: "text", text }], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + case "history": { + try { + const limit = args.limit || 20; + const response = await brokerFetch( + `/history/${myId}/${args.partnerId}?limit=${limit}` + ); + + if (!response.messages?.length) { + return { + content: [ + { + type: "text", + text: `Pas d'historique avec ${args.partnerId}.`, + }, + ], + }; + } + + let text = `**Historique avec ${args.partnerId}:**\n\n`; + // Inverser pour avoir l'ordre chronologique + const messages = response.messages.reverse(); + for (const msg of messages) { + const date = new Date(msg.created_at).toLocaleString(); + text += `[${date}] **${msg.from_id}:** ${msg.content}\n\n`; + } + + return { + content: [{ type: "text", text }], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Erreur: ${error.message}` }], + isError: true, + }; + } + } + + default: + return { + content: [{ type: "text", text: `Tool inconnu: ${name}` }], + isError: true, + }; + } +}); + +// Démarrer +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(`[MCP-PARTNER] Started (ID: ${myId})`); +} + +main().catch(console.error); diff --git a/mcp-slave/index.js b/mcp-slave/index.js deleted file mode 100644 index 36926de..0000000 --- a/mcp-slave/index.js +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { writeFileSync, mkdirSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; - -const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210"; -const PARTNER_NAME = process.env.PARTNER_NAME || process.env.SLAVE_NAME || "Partner"; - -// ID persistant basé sur le dossier de travail (unique par projet) -const cwd = process.cwd(); -const projectName = cwd.split(/[/\\]/).pop().toLowerCase().replace(/[^a-z0-9]/g, "_"); -const partnerId = projectName || PARTNER_NAME.toLowerCase().replace(/[^a-z0-9]/g, "_"); - -// Fichier state unique pour le hook -const __dirname = dirname(fileURLToPath(import.meta.url)); -const stateDir = join(__dirname, "..", ".state"); -const stateFile = join(stateDir, "current.json"); // Fichier unique, pas basé sur l'ID - -// Créer le dossier state -try { - mkdirSync(stateDir, { recursive: true }); -} catch {} - -let isConnected = false; -let currentRequestId = null; - -/** - * Appel HTTP au broker - * @param {string} path - endpoint - * @param {object} options - fetch options - * @param {number} timeoutMs - timeout en ms (défaut: 30s, 0 = pas de timeout) - */ -async function brokerFetch(path, options = {}, timeoutMs = 30000) { - const url = `${BROKER_URL}${path}`; - - const fetchOptions = { - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }; - - // Ajouter un AbortController pour le timeout si spécifié - if (timeoutMs > 0) { - const controller = new AbortController(); - setTimeout(() => controller.abort(), timeoutMs); - fetchOptions.signal = controller.signal; - } - - const response = await fetch(url, fetchOptions); - return response.json(); -} - -/** - * Sauvegarde l'état pour le hook - */ -function saveState() { - const state = { - partnerId, - partnerName: PARTNER_NAME, - currentRequestId, - brokerUrl: BROKER_URL, - }; - writeFileSync(stateFile, JSON.stringify(state, null, 2)); -} - -// Créer le serveur MCP -const server = new Server( - { - name: "mcp-claude-duo-slave", - version: "1.0.0", - }, - { - capabilities: { - tools: {}, - }, - } -); - -// Liste des tools -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "connect", - description: - "Se connecte en tant que partenaire et attend les messages. Utilise cet outil pour te connecter puis pour attendre chaque nouveau message de ton interlocuteur.", - inputSchema: { - type: "object", - properties: { - name: { - type: "string", - description: "Ton nom/pseudo pour cette conversation (optionnel)", - }, - }, - }, - }, - { - name: "respond", - description: - "Envoie ta réponse à ton interlocuteur. Utilise cet outil après avoir reçu un message pour lui répondre.", - inputSchema: { - type: "object", - properties: { - message: { - type: "string", - description: "Ta réponse à envoyer", - }, - }, - required: ["message"], - }, - }, - { - name: "disconnect", - description: "Se déconnecte de la conversation", - inputSchema: { - type: "object", - properties: {}, - }, - }, - ], - }; -}); - -// Handler des tools -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - switch (name) { - case "connect": { - try { - // S'enregistrer si pas encore fait - if (!isConnected) { - const name = args.name || PARTNER_NAME; - await brokerFetch("/register", { - method: "POST", - body: JSON.stringify({ partnerId, name }), - }); - isConnected = true; - console.error(`[MCP-SLAVE] Registered as ${name} (${partnerId})`); - } - - // Attendre un message (long-polling sans timeout) - console.error("[MCP-SLAVE] Waiting for message..."); - const response = await brokerFetch(`/wait/${partnerId}`, {}, 0); - - if (response.disconnected) { - isConnected = false; - return { - content: [{ type: "text", text: "Déconnecté." }], - }; - } - - if (!response.hasMessage) { - // Timeout, pas de message - réessayer - return { - content: [ - { - type: "text", - text: "Pas de nouveau message. Rappelle `connect` pour continuer à attendre.", - }, - ], - }; - } - - // Message reçu ! - currentRequestId = response.message.requestId; - saveState(); // Sauvegarder pour le hook - - return { - content: [ - { - type: "text", - text: `**Message reçu:** ${response.message.content}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Erreur de connexion au broker: ${error.message}. Le broker est-il lancé ?`, - }, - ], - isError: true, - }; - } - } - - case "respond": { - try { - if (!currentRequestId) { - return { - content: [ - { - type: "text", - text: "Aucun message en attente de réponse. Utilise `connect` d'abord pour recevoir un message.", - }, - ], - }; - } - - // Envoyer la réponse au broker - await brokerFetch("/respond", { - method: "POST", - body: JSON.stringify({ - partnerId, - requestId: currentRequestId, - content: args.message, - }), - }); - - const oldRequestId = currentRequestId; - currentRequestId = null; - saveState(); - - return { - content: [ - { - type: "text", - text: `Réponse envoyée. Utilise \`connect\` pour attendre le prochain message.`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Erreur d'envoi: ${error.message}`, - }, - ], - isError: true, - }; - } - } - - case "disconnect": { - try { - if (isConnected) { - await brokerFetch("/disconnect", { - method: "POST", - body: JSON.stringify({ partnerId }), - }); - isConnected = false; - } - return { - content: [{ type: "text", text: "Déconnecté." }], - }; - } catch (error) { - return { - content: [{ type: "text", text: `Erreur: ${error.message}` }], - isError: true, - }; - } - } - - default: - return { - content: [{ type: "text", text: `Tool inconnu: ${name}` }], - isError: true, - }; - } -}); - -// Démarrer -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`[MCP-SLAVE] Started (ID: ${partnerId})`); -} - -main().catch(console.error); diff --git a/package-lock.json b/package-lock.json index ce6dc81..2ff0db0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "mcp-claude-duo", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcp-claude-duo", - "version": "1.0.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", + "better-sqlite3": "^11.0.0", "express": "^4.18.2" } }, @@ -385,6 +386,57 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -436,6 +488,30 @@ "node": ">= 0.8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -474,6 +550,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -550,6 +632,30 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -569,6 +675,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -598,6 +713,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -664,6 +788,15 @@ "node": ">=18.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -748,6 +881,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -784,6 +923,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -830,6 +975,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -912,12 +1063,38 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1029,12 +1206,45 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1044,6 +1254,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1119,6 +1341,32 @@ "node": ">=16.20.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1132,6 +1380,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -1171,6 +1429,35 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1255,6 +1542,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -1399,6 +1698,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1408,6 +1752,52 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1417,6 +1807,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1439,6 +1841,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index d2bfbb3..306de46 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mcp-claude-duo", - "version": "1.0.0", - "description": "Deux instances Claude Code qui discutent ensemble via MCP", + "version": "2.0.0", + "description": "MCP pour faire discuter plusieurs instances Claude Code ensemble", "type": "module", "scripts": { "broker": "node broker/index.js", @@ -9,6 +9,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", + "better-sqlite3": "^11.0.0", "express": "^4.18.2" }, "keywords": ["mcp", "claude", "duo", "conversation"],