poltertab 1.8.0-beta.2 → 1.8.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp-server/bridge.js +24 -47
- package/package.json +1 -1
package/mcp-server/bridge.js
CHANGED
|
@@ -54,7 +54,7 @@ let isSecondary = false;
|
|
|
54
54
|
let nodeId = null;
|
|
55
55
|
let wss = null;
|
|
56
56
|
let httpServer = null;
|
|
57
|
-
|
|
57
|
+
// (httpServer6 removed — wildcard bind covers ::1; see fix/legacy-bind)
|
|
58
58
|
|
|
59
59
|
// Secondary-side retry state. One counter for the whole reconnect/probe/promote
|
|
60
60
|
// cycle, reset only by a Primary's secondary_accepted; `refusal` and
|
|
@@ -216,8 +216,14 @@ function allowOrigin(info, cb) {
|
|
|
216
216
|
|
|
217
217
|
function setupPrimaryServer() {
|
|
218
218
|
const http = require("http");
|
|
219
|
+
const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
219
220
|
const handleRequest = (req, res) => {
|
|
220
221
|
if (req.method === "GET" && req.url === "/status") {
|
|
222
|
+
if (!LOOPBACK.has(req.socket.remoteAddress)) {
|
|
223
|
+
res.writeHead(404);
|
|
224
|
+
res.end();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
221
227
|
const now = Date.now();
|
|
222
228
|
const secondaries = [];
|
|
223
229
|
for (const [nid, ws] of secondaryClients.entries()) {
|
|
@@ -268,26 +274,20 @@ function setupPrimaryServer() {
|
|
|
268
274
|
httpServer.close();
|
|
269
275
|
httpServer = null;
|
|
270
276
|
startSecondaryMode();
|
|
271
|
-
// NICE-2 residual race: a legacy (wildcard-binding) PolterTab that starts
|
|
272
|
-
// in the ms between a successful probe and our listen() call will bind
|
|
273
|
-
// undetected on macOS — SO_REUSEADDR lets 127.0.0.1 bind over 0.0.0.0,
|
|
274
|
-
// so the new loopback Primary dual-binds silently. That window is
|
|
275
|
-
// milliseconds and requires two simultaneous starts; not worth a retry
|
|
276
|
-
// loop, but do not remove the probe and rely on this handler alone.
|
|
277
277
|
} else {
|
|
278
278
|
console.error(`[PolterTab MCP] HTTP server error: ${e.message}`);
|
|
279
279
|
}
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
-
// Why probe-first (SHOULD-1 narrative
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// EADDRINUSE handler above
|
|
282
|
+
// Why probe-first (SHOULD-1 narrative):
|
|
283
|
+
// We bind :: (IPv6 wildcard, dual-stack) so a second wildcard bind from a
|
|
284
|
+
// legacy 1.7.x/beta.1 server gets EADDRINUSE on every platform. But on
|
|
285
|
+
// macOS SO_REUSEADDR, a wildcard bind does NOT give EADDRINUSE when the
|
|
286
|
+
// existing listener is a specific-address bind (127.0.0.1/::1) — the old
|
|
287
|
+
// regression this PR fixes. The probe detects a real primary already
|
|
288
|
+
// serving 127.0.0.1 and demotes us before we call listen(), avoiding a
|
|
289
|
+
// race in the milliseconds between a successful probe and our own bind.
|
|
290
|
+
// The EADDRINUSE handler above covers the symmetric case.
|
|
291
291
|
probePrimary().then((alive) => {
|
|
292
292
|
if (alive) {
|
|
293
293
|
console.error(
|
|
@@ -298,7 +298,7 @@ function setupPrimaryServer() {
|
|
|
298
298
|
startSecondaryMode();
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
|
-
httpServer.listen(WS_PORT,
|
|
301
|
+
httpServer.listen(WS_PORT, () => {
|
|
302
302
|
wss = new WebSocket.WebSocketServer({
|
|
303
303
|
server: httpServer,
|
|
304
304
|
verifyClient: allowOrigin,
|
|
@@ -409,35 +409,13 @@ function setupPrimaryServer() {
|
|
|
409
409
|
|
|
410
410
|
setupPrimaryWss(wss);
|
|
411
411
|
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
//
|
|
420
|
-
// The "listening" log is delayed until ::1 is ready (or skipped) so that
|
|
421
|
-
// any client connecting on the log signal can reach both addresses.
|
|
422
|
-
const SKIP_IPV6 = new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT", "EPROTONOSUPPORT"]);
|
|
423
|
-
const logListening = () =>
|
|
424
|
-
console.error(
|
|
425
|
-
`[PolterTab MCP] Primary WebSocket server listening on ws://localhost:${WS_PORT}`,
|
|
426
|
-
);
|
|
427
|
-
httpServer6 = http.createServer(handleRequest);
|
|
428
|
-
httpServer6.on("error", (e) => {
|
|
429
|
-
if (SKIP_IPV6.has(e.code)) {
|
|
430
|
-
httpServer6 = null;
|
|
431
|
-
} else {
|
|
432
|
-
console.error(`[PolterTab MCP] IPv6 loopback error: ${e.message}`);
|
|
433
|
-
}
|
|
434
|
-
logListening();
|
|
435
|
-
});
|
|
436
|
-
httpServer6.once("listening", logListening);
|
|
437
|
-
httpServer6.on("upgrade", (req, socket, head) => {
|
|
438
|
-
wss.handleUpgrade(req, socket, head, (client) => wss.emit("connection", client, req));
|
|
439
|
-
});
|
|
440
|
-
httpServer6.listen(WS_PORT, "::1");
|
|
412
|
+
// Wildcard bind (:: dual-stack) covers both 127.0.0.1 and ::1, so Node 18
|
|
413
|
+
// clients that resolve localhost → ::1 reach the primary without a separate
|
|
414
|
+
// httpServer6. A legacy server attempting listen(port) with no host gets
|
|
415
|
+
// EADDRINUSE because two wildcard binds on the same port always collide.
|
|
416
|
+
console.error(
|
|
417
|
+
`[PolterTab MCP] Primary WebSocket server listening on ws://localhost:${WS_PORT}`,
|
|
418
|
+
);
|
|
441
419
|
});
|
|
442
420
|
});
|
|
443
421
|
}
|
|
@@ -1125,7 +1103,6 @@ function shutdown() {
|
|
|
1125
1103
|
console.error("[PolterTab MCP] Shutting down...");
|
|
1126
1104
|
if (wss) wss.close();
|
|
1127
1105
|
if (httpServer) httpServer.close();
|
|
1128
|
-
if (httpServer6) httpServer6.close();
|
|
1129
1106
|
process.exit(0);
|
|
1130
1107
|
}
|
|
1131
1108
|
// Helper to wait for WebSocket connection
|
package/package.json
CHANGED