mcp-hub-002: WebSocket backend registration and health tracking

This commit is contained in:
Agent 2026-03-12 16:46:30 +00:00
parent 60d92af13b
commit a93bcfffa7
4 changed files with 138 additions and 2 deletions

47
src/backend-registry.js Normal file
View file

@ -0,0 +1,47 @@
class BackendRegistry {
constructor() {
this.backends = new Map();
}
register(serviceId, ws) {
this.backends.set(serviceId, {
ws,
connectedAt: new Date().toISOString(),
lastPing: null,
});
console.log(`[registry] registered backend: ${serviceId}`);
}
unregister(serviceId) {
if (this.backends.has(serviceId)) {
this.backends.delete(serviceId);
console.log(`[registry] unregistered backend: ${serviceId}`);
}
}
get(serviceId) {
const entry = this.backends.get(serviceId);
return entry ? entry.ws : null;
}
list() {
return Array.from(this.backends.entries()).map(([serviceId, entry]) => ({
serviceId,
connectedAt: entry.connectedAt,
lastPing: entry.lastPing,
}));
}
updatePing(serviceId) {
const entry = this.backends.get(serviceId);
if (entry) {
entry.lastPing = new Date().toISOString();
}
}
count() {
return this.backends.size;
}
}
module.exports = new BackendRegistry();

View file

@ -1,6 +1,11 @@
const http = require('http');
const app = require('./server'); const app = require('./server');
const config = require('./config'); const config = require('./config');
const setupWsServer = require('./ws-server');
app.listen(config.PORT, () => { const httpServer = http.createServer(app);
setupWsServer(httpServer);
httpServer.listen(config.PORT, () => {
console.log(`MCP relay hub listening on port ${config.PORT}`); console.log(`MCP relay hub listening on port ${config.PORT}`);
}); });

View file

@ -1,4 +1,5 @@
const express = require('express'); const express = require('express');
const registry = require('./backend-registry');
const app = express(); const app = express();
@ -16,7 +17,7 @@ app.get('/health', (req, res) => {
res.json({ res.json({
status: 'ok', status: 'ok',
uptime: process.uptime(), uptime: process.uptime(),
connectedBackends: 0, connectedBackends: registry.count(),
}); });
}); });

83
src/ws-server.js Normal file
View file

@ -0,0 +1,83 @@
const WebSocket = require('ws');
const config = require('./config');
const registry = require('./backend-registry');
const PING_INTERVAL_MS = 30000;
const MAX_MISSED_PONGS = 2;
function setupWsServer(httpServer) {
const wss = new WebSocket.Server({ noServer: true });
httpServer.on('upgrade', (req, socket, head) => {
if (req.url !== '/ws/register') {
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
wss.on('connection', (ws) => {
let serviceId = null;
let authenticated = false;
let missedPongs = 0;
let pingTimer = null;
ws.once('message', (data) => {
let msg;
try {
msg = JSON.parse(data);
} catch {
ws.close(4001, 'unauthorized');
return;
}
if (msg.type !== 'register' || !msg.serviceId || msg.secret !== config.HUB_SECRET) {
ws.close(4001, 'unauthorized');
return;
}
serviceId = msg.serviceId;
authenticated = true;
registry.register(serviceId, ws);
ws.on('message', (payload) => {
// MCP responses — stored for relay (handled in next task)
});
ws.on('pong', () => {
missedPongs = 0;
registry.updatePing(serviceId);
});
// Heartbeat
pingTimer = setInterval(() => {
if (missedPongs >= MAX_MISSED_PONGS) {
console.log(`[ws] backend ${serviceId} missed ${missedPongs} pongs, disconnecting`);
ws.terminate();
return;
}
if (ws.readyState === WebSocket.OPEN) {
missedPongs++;
ws.ping();
}
}, PING_INTERVAL_MS);
});
ws.on('close', () => {
if (pingTimer) clearInterval(pingTimer);
if (authenticated && serviceId) {
registry.unregister(serviceId);
}
});
ws.on('error', (err) => {
console.error(`[ws] error on ${serviceId || 'unauthenticated'}:`, err.message);
});
});
return wss;
}
module.exports = setupWsServer;