Skip to main content
Glama
mcp-server.js11.3 kB
#!/usr/bin/env node /** * Claude Relay MCP Server * * MCP server that provides tools for Claude Code to communicate * with peer instances via the WebSocket relay. * * Usage: node mcp-server.js [--client-id=M2] [--relay-url=ws://localhost:9999] * * Environment variables: * RELAY_CLIENT_ID - Client identifier (M1, M2, etc.) * RELAY_URL - WebSocket relay server URL */ const WebSocket = require('ws'); const readline = require('readline'); const os = require('os'); // Configuration from args or env const args = process.argv.slice(2).reduce((acc, arg) => { const [key, val] = arg.replace(/^--/, '').split('='); acc[key] = val; return acc; }, {}); const CLIENT_ID = args['client-id'] || process.env.RELAY_CLIENT_ID || os.hostname().split('.')[0].toUpperCase(); const RELAY_URL = args['relay-url'] || process.env.RELAY_URL || 'ws://localhost:9999'; // State let ws = null; let connected = false; let peers = []; let pendingMessages = []; let messageQueue = []; // MCP protocol handler const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Read JSON-RPC messages from stdin let buffer = ''; rl.on('line', (line) => { buffer += line; try { const message = JSON.parse(buffer); buffer = ''; handleMcpMessage(message); } catch { // Incomplete JSON, wait for more } }); function sendMcpResponse(response) { const json = JSON.stringify(response); process.stdout.write(json + '\n'); } function handleMcpMessage(message) { const { id, method, params } = message; switch (method) { case 'initialize': sendMcpResponse({ jsonrpc: '2.0', id, result: { protocolVersion: '2024-11-05', serverInfo: { name: 'claude-relay', version: '1.0.0' }, capabilities: { tools: {} } } }); // Connect to relay after initialization connectToRelay(); break; case 'notifications/initialized': // Client acknowledged initialization break; case 'tools/list': sendMcpResponse({ jsonrpc: '2.0', id, result: { tools: [ { name: 'relay_send', description: `Send a message to peer Claude Code instance(s). You are ${CLIENT_ID}.`, inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'Message content to send to peer' }, to: { type: 'string', description: 'Target peer ID (e.g., "M1" or "M2") or "all" for broadcast. Default: all' } }, required: ['message'] } }, { name: 'relay_receive', description: 'Get recent messages from peer Claude Code instance(s)', inputSchema: { type: 'object', properties: { count: { type: 'number', description: 'Maximum number of messages to retrieve (default: 10)' }, from: { type: 'string', description: 'Filter messages by sender ID (optional)' } } } }, { name: 'relay_peers', description: 'List currently connected peer Claude Code instances', inputSchema: { type: 'object', properties: {} } }, { name: 'relay_status', description: 'Check connection status to the relay server', inputSchema: { type: 'object', properties: {} } } ] } }); break; case 'tools/call': handleToolCall(id, params.name, params.arguments || {}); break; default: sendMcpResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } } function handleToolCall(requestId, toolName, args) { switch (toolName) { case 'relay_send': if (!connected) { sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: `Error: Not connected to relay server at ${RELAY_URL}. Is the server running?` }] } }); return; } ws.send(JSON.stringify({ type: 'message', to: args.to || 'all', content: args.message })); sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: `Message sent to ${args.to || 'all peers'}: "${args.message.substring(0, 100)}${args.message.length > 100 ? '...' : ''}"` }] } }); break; case 'relay_receive': if (!connected) { sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: `Error: Not connected to relay server` }] } }); return; } // Request history from server const historyRequestId = Date.now(); pendingMessages.push({ requestId, type: 'history', id: historyRequestId }); ws.send(JSON.stringify({ type: 'get_history', count: args.count || 10, from: args.from })); // Set timeout for response setTimeout(() => { const idx = pendingMessages.findIndex(p => p.id === historyRequestId); if (idx !== -1) { pendingMessages.splice(idx, 1); sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: 'Timeout waiting for history from relay server' }] } }); } }, 5000); break; case 'relay_peers': if (!connected) { sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: `Not connected to relay server. Unable to list peers.` }] } }); return; } // Request current peers const peersRequestId = Date.now(); pendingMessages.push({ requestId, type: 'peers', id: peersRequestId }); ws.send(JSON.stringify({ type: 'get_peers' })); setTimeout(() => { const idx = pendingMessages.findIndex(p => p.id === peersRequestId); if (idx !== -1) { pendingMessages.splice(idx, 1); sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: `Connected peers (cached): ${peers.length > 0 ? peers.join(', ') : 'none'}` }] } }); } }, 3000); break; case 'relay_status': sendMcpResponse({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: connected ? `Connected to ${RELAY_URL} as "${CLIENT_ID}". Peers online: ${peers.length > 0 ? peers.filter(p => p !== CLIENT_ID).join(', ') || 'none' : 'checking...'}` : `Disconnected from relay server. Attempting to connect to ${RELAY_URL}...` }] } }); break; default: sendMcpResponse({ jsonrpc: '2.0', id: requestId, error: { code: -32601, message: `Unknown tool: ${toolName}` } }); } } function connectToRelay() { if (ws) { ws.close(); } ws = new WebSocket(RELAY_URL); ws.on('open', () => { connected = true; // Register with relay ws.send(JSON.stringify({ type: 'register', clientId: CLIENT_ID })); }); ws.on('message', (data) => { try { const msg = JSON.parse(data.toString()); switch (msg.type) { case 'registered': peers = msg.peers || []; break; case 'peers': peers = msg.peers || []; // Respond to pending peers request const peersReq = pendingMessages.find(p => p.type === 'peers'); if (peersReq) { pendingMessages = pendingMessages.filter(p => p !== peersReq); sendMcpResponse({ jsonrpc: '2.0', id: peersReq.requestId, result: { content: [{ type: 'text', text: `You are: ${msg.self}\nConnected peers: ${peers.filter(p => p !== msg.self).join(', ') || 'none'}` }] } }); } break; case 'history': const histReq = pendingMessages.find(p => p.type === 'history'); if (histReq) { pendingMessages = pendingMessages.filter(p => p !== histReq); const messages = msg.messages || []; let text = messages.length > 0 ? messages.map(m => `[${m.timestamp}] ${m.from}: ${m.content}`).join('\n') : 'No messages in history'; sendMcpResponse({ jsonrpc: '2.0', id: histReq.requestId, result: { content: [{ type: 'text', text }] } }); } break; case 'peer_joined': peers = msg.peers || []; // Queue notification for next relay_receive messageQueue.push({ type: 'system', content: `Peer "${msg.clientId}" joined`, timestamp: new Date().toISOString() }); break; case 'peer_left': peers = msg.peers || []; messageQueue.push({ type: 'system', content: `Peer "${msg.clientId}" left`, timestamp: new Date().toISOString() }); break; case 'message': // Incoming message from peer - queue it messageQueue.push({ from: msg.from, content: msg.content, timestamp: msg.timestamp }); break; case 'error': // Log errors but don't interrupt break; } } catch { // Ignore parse errors } }); ws.on('close', () => { connected = false; peers = []; // Attempt reconnect after delay setTimeout(connectToRelay, 5000); }); ws.on('error', () => { // Error will trigger close, which handles reconnect }); } // Handle shutdown process.on('SIGINT', () => { if (ws) ws.close(); process.exit(0); }); process.on('SIGTERM', () => { if (ws) ws.close(); process.exit(0); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gvorwaller/claude-relay'

If you have feedback or need assistance with the MCP directory API, please join our Discord server