Initial commit - MCP Claude Duo

Permet à deux instances Claude Code de discuter ensemble via MCP.

Architecture:
- broker/ : Serveur HTTP central avec heartbeat
- mcp-master/ : MCP pour envoyer des messages (talk, list_partners)
- mcp-slave/ : MCP pour recevoir et répondre (connect, respond)
- hooks/ : Hook Stop pour capturer les réponses (optionnel)

Features:
- Long-polling sans timeout
- Heartbeat toutes les 30s (fix bug Claude Code SSE)
- Sauvegarde des conversations par date
- ID basé sur le dossier de travail (unique par projet)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-24 03:42:25 +07:00
commit b7766853cf
8 changed files with 2573 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.state/
conversations/
*.log

156
README.md Normal file
View File

@ -0,0 +1,156 @@
# MCP Claude Duo
Fait discuter deux instances Claude Code ensemble via MCP.
## Architecture
```
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) │
└─────────────────┘ └───────┘ └─────────────────┘
```
## Installation
```bash
cd mcp-claude-duo
npm install
```
## Démarrage
### 1. Lancer le broker (dans un terminal séparé)
```bash
npm run broker
```
Le broker tourne sur `http://localhost:3210` par défaut.
### 2. Configurer le Master (Terminal 1)
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"
}
}
}
}
```
### 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
```
## Tools disponibles
### MCP Master
| Tool | Description |
|------|-------------|
| `talk(message)` | Envoie un message et attend la réponse |
| `list_partners()` | Liste les partenaires connectés |
### MCP Slave
| Tool | Description |
|------|-------------|
| `connect(name?)` | Se connecte et attend les messages |
| `disconnect()` | Se déconnecte |
## Variables d'environnement
| Variable | Description | Défaut |
|----------|-------------|--------|
| `BROKER_URL` | URL du broker | `http://localhost:3210` |
| `BROKER_PORT` | Port du broker | `3210` |
| `SLAVE_NAME` | Nom du slave | `Partner` |
## Flow détaillé
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
## License
MIT

311
broker/index.js Normal file
View File

@ -0,0 +1,311 @@
import express from "express";
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
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 } }
const pendingResponses = new Map();
// Long-polling requests en attente: { partnerId: { res, timeout } }
const waitingPartners = new Map();
/**
* Slave s'enregistre
* POST /register
* Body: { partnerId, name }
*/
app.post("/register", (req, res) => {
const { partnerId, name } = req.body;
if (!partnerId) {
return res.status(400).json({ error: "partnerId required" });
}
partners.set(partnerId, {
name: name || partnerId,
connectedAt: Date.now(),
lastSeen: Date.now(),
status: "connected",
});
pendingMessages.set(partnerId, []);
console.log(`[BROKER] Slave registered: ${name || partnerId} (${partnerId})`);
res.json({ success: true, message: "Registered" });
});
/**
* Slave attend un message (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" });
}
// Check s'il y a déjà un message en attente
const messages = pendingMessages.get(partnerId) || [];
if (messages.length > 0) {
const msg = messages.shift();
return res.json({ hasMessage: true, message: msg });
}
// 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" });
}
// Heartbeat toutes les 30 secondes pour garder la connexion vivante (bug Claude Code)
const heartbeat = setInterval(() => {
try {
res.write(": heartbeat\n\n");
} catch (e) {
clearInterval(heartbeat);
}
}, 30000);
// Nettoyer quand la connexion se ferme
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}`);
});
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)
* POST /respond
* Body: { partnerId, requestId, content }
*/
app.post("/respond", (req, res) => {
const { partnerId, requestId, content } = req.body;
console.log(`[BROKER] ${partnerId} responded: "${content.substring(0, 50)}..."`);
// Sauvegarder la réponse du partner
if (partnerId && content) {
saveMessage(partnerId, partnerId, content);
}
if (requestId && pendingResponses.has(requestId)) {
const { resolve, timeout } = pendingResponses.get(requestId);
clearTimeout(timeout);
pendingResponses.delete(requestId);
resolve({ success: true, content });
}
res.json({ success: true });
});
/**
* Liste les partners connectés
* GET /partners
*/
app.get("/partners", (req, res) => {
const list = [];
for (const [id, info] of partners) {
list.push({ id, ...info });
}
res.json({ partners: list });
});
/**
* Slave se déconnecte
* POST /disconnect
*/
app.post("/disconnect", (req, res) => {
const { partnerId } = req.body;
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);
});
/**
* Health check
*/
app.get("/health", (req, res) => {
res.json({ status: "ok", partners: partners.size });
});
app.listen(PORT, () => {
console.log(`[BROKER] Claude Duo Broker running on http://localhost:${PORT}`);
});

109
hooks/on-stop.js Normal file
View File

@ -0,0 +1,109 @@
#!/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();

194
mcp-master/index.js Normal file
View File

@ -0,0 +1,194 @@
#!/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);

282
mcp-slave/index.js Normal file
View File

@ -0,0 +1,282 @@
#!/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);

1501
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "mcp-claude-duo",
"version": "1.0.0",
"description": "Deux instances Claude Code qui discutent ensemble via MCP",
"type": "module",
"scripts": {
"broker": "node broker/index.js",
"start:broker": "node broker/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"express": "^4.18.2"
},
"keywords": ["mcp", "claude", "duo", "conversation"],
"license": "MIT"
}