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>
195 lines
4.7 KiB
JavaScript
195 lines
4.7 KiB
JavaScript
#!/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);
|