abu-browser-bridge 0.1.0 → 0.2.0

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.
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * WebSocket server that accepts connections from the Chrome Extension.
3
3
  * Routes requests from MCP tools to the extension and returns responses.
4
+ *
5
+ * Also exposes a lightweight HTTP discovery endpoint on a fixed port
6
+ * so the Chrome Extension can reliably find the WS port.
4
7
  */
5
8
  import type { BridgeResponse } from './types.js';
6
9
  /**
7
- * Start the WebSocket server. If the default port is occupied (by a stale bridge),
8
- * try a few alternative ports. The Extension will discover the port via a well-known range.
10
+ * Start the WebSocket server. If the default port is occupied,
11
+ * try a few alternative ports. The Extension discovers the port
12
+ * via the HTTP discovery endpoint.
9
13
  */
10
14
  export declare function startWSServer(port?: number): Promise<number>;
11
15
  /**
package/dist/wsServer.js CHANGED
@@ -1,14 +1,22 @@
1
1
  /**
2
2
  * WebSocket server that accepts connections from the Chrome Extension.
3
3
  * Routes requests from MCP tools to the extension and returns responses.
4
+ *
5
+ * Also exposes a lightweight HTTP discovery endpoint on a fixed port
6
+ * so the Chrome Extension can reliably find the WS port.
4
7
  */
5
8
  import { WebSocketServer, WebSocket } from 'ws';
9
+ import { createServer } from 'http';
6
10
  import { createConnection } from 'net';
7
- const DEFAULT_PORT = 9876;
8
- const HEARTBEAT_INTERVAL = 30_000;
11
+ const DEFAULT_WS_PORT = 9876;
12
+ const DISCOVERY_PORT = 9875;
13
+ const HEARTBEAT_INTERVAL = 15_000; // 15s — more frequent for faster disconnect detection
14
+ const PONG_TIMEOUT = 5_000; // If no pong within 5s, consider dead
9
15
  let wss = null;
16
+ let discoveryServer = null;
10
17
  let extensionSocket = null;
11
18
  let heartbeatTimer = null;
19
+ let pongReceived = true;
12
20
  const pendingRequests = new Map();
13
21
  let requestCounter = 0;
14
22
  let activePort = null;
@@ -17,7 +25,6 @@ function generateId() {
17
25
  }
18
26
  /**
19
27
  * Check if a port is in use by trying to connect to it.
20
- * Cross-platform — works on macOS, Windows, Linux.
21
28
  */
22
29
  function isPortInUse(port, host) {
23
30
  return new Promise((resolve) => {
@@ -36,11 +43,65 @@ function isPortInUse(port, host) {
36
43
  });
37
44
  });
38
45
  }
46
+ // --- HTTP Discovery Endpoint ---
39
47
  /**
40
- * Start the WebSocket server. If the default port is occupied (by a stale bridge),
41
- * try a few alternative ports. The Extension will discover the port via a well-known range.
48
+ * Start the HTTP discovery server on a fixed well-known port.
49
+ * Chrome Extension queries this to find the actual WS port.
50
+ *
51
+ * GET /status → { wsPort, pid, extensionConnected, uptime }
42
52
  */
43
- export async function startWSServer(port = DEFAULT_PORT) {
53
+ function startDiscoveryServer() {
54
+ return new Promise((resolve, reject) => {
55
+ const startTime = Date.now();
56
+ discoveryServer = createServer((req, res) => {
57
+ // CORS headers for Chrome Extension fetch
58
+ res.setHeader('Access-Control-Allow-Origin', '*');
59
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
60
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
61
+ if (req.method === 'OPTIONS') {
62
+ res.writeHead(204);
63
+ res.end();
64
+ return;
65
+ }
66
+ if (req.url === '/status' || req.url === '/') {
67
+ res.writeHead(200, { 'Content-Type': 'application/json' });
68
+ res.end(JSON.stringify({
69
+ wsPort: activePort,
70
+ pid: process.pid,
71
+ extensionConnected: isExtensionConnected(),
72
+ uptime: Math.round((Date.now() - startTime) / 1000),
73
+ version: '0.1.1',
74
+ }));
75
+ return;
76
+ }
77
+ res.writeHead(404);
78
+ res.end('Not found');
79
+ });
80
+ discoveryServer.on('error', (err) => {
81
+ if (err.code === 'EADDRINUSE') {
82
+ console.error(`[abu-bridge] Discovery port ${DISCOVERY_PORT} in use — old bridge still running?`);
83
+ // Not fatal — extension can still fall back to port scanning
84
+ resolve();
85
+ }
86
+ else {
87
+ reject(err);
88
+ }
89
+ });
90
+ discoveryServer.listen(DISCOVERY_PORT, '127.0.0.1', () => {
91
+ console.error(`[abu-bridge] Discovery endpoint: http://127.0.0.1:${DISCOVERY_PORT}/status`);
92
+ resolve();
93
+ });
94
+ });
95
+ }
96
+ // --- WebSocket Server ---
97
+ /**
98
+ * Start the WebSocket server. If the default port is occupied,
99
+ * try a few alternative ports. The Extension discovers the port
100
+ * via the HTTP discovery endpoint.
101
+ */
102
+ export async function startWSServer(port = DEFAULT_WS_PORT) {
103
+ // Start discovery endpoint first (non-blocking if port taken)
104
+ await startDiscoveryServer();
44
105
  const MAX_RETRIES = 5;
45
106
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
46
107
  const tryPort = port + attempt;
@@ -56,7 +117,6 @@ export async function startWSServer(port = DEFAULT_PORT) {
56
117
  }
57
118
  catch (err) {
58
119
  const msg = err instanceof Error ? err.message : String(err);
59
- // EADDRINUSE can still happen in a race condition
60
120
  if (msg.includes('EADDRINUSE') && attempt < MAX_RETRIES - 1) {
61
121
  console.error(`[abu-bridge] Port ${tryPort} race condition, trying next...`);
62
122
  continue;
@@ -87,6 +147,11 @@ function listenOnPort(port) {
87
147
  extensionSocket.close(1000, 'Replaced by new connection');
88
148
  }
89
149
  extensionSocket = ws;
150
+ pongReceived = true;
151
+ // Handle pong responses for heartbeat
152
+ ws.on('pong', () => {
153
+ pongReceived = true;
154
+ });
90
155
  ws.on('message', (data) => {
91
156
  try {
92
157
  const msg = JSON.parse(data.toString());
@@ -117,6 +182,14 @@ function listenOnPort(port) {
117
182
  function startHeartbeat() {
118
183
  heartbeatTimer = setInterval(() => {
119
184
  if (extensionSocket && extensionSocket.readyState === WebSocket.OPEN) {
185
+ if (!pongReceived) {
186
+ // No pong since last ping — connection is dead
187
+ console.error('[abu-bridge] Extension not responding to heartbeat, closing connection');
188
+ extensionSocket.terminate();
189
+ extensionSocket = null;
190
+ return;
191
+ }
192
+ pongReceived = false;
120
193
  extensionSocket.ping();
121
194
  }
122
195
  }, HEARTBEAT_INTERVAL);
@@ -180,5 +253,9 @@ export function stopWSServer() {
180
253
  wss.close();
181
254
  wss = null;
182
255
  }
256
+ if (discoveryServer) {
257
+ discoveryServer.close();
258
+ discoveryServer = null;
259
+ }
183
260
  activePort = null;
184
261
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abu-browser-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP Server that bridges Abu AI assistant with Chrome Extension for browser automation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "keywords": ["mcp", "browser", "automation", "chrome-extension", "abu"],
13
13
  "bin": {
14
- "abu-browser-bridge": "./dist/index.js"
14
+ "abu-browser-bridge": "dist/index.js"
15
15
  },
16
16
  "files": [
17
17
  "dist"