@businessmaps/bifrost 0.0.2 → 0.1.2

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/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
 
6
6
  MCP server that lets AI tools call functions running in your browser.
7
7
 
8
- The daemon sits between any MCP client and your browser tab. It speaks JSON-RPC over stdio on one side and WebSocket on the other. Your browser app connects, registers tools, and the AI can call them. No dependencies, just Node 18+.
8
+ The daemon sits between any MCP client and your browser tab. It speaks JSON-RPC over stdio on one side and WebSocket on the other. Your browser app connects, registers tools, and the AI can call them.
9
+
10
+ **Requirements:** Node.js 18+ · [`ws`](https://github.com/websockets/ws) (installed automatically)
9
11
 
10
12
  ## Install
11
13
 
@@ -13,18 +15,14 @@ The daemon sits between any MCP client and your browser tab. It speaks JSON-RPC
13
15
  npm i -g @businessmaps/bifrost
14
16
  ```
15
17
 
16
- or without npm
17
-
18
- ```bash
19
- curl -fsSL https://raw.githubusercontent.com/Business-Maps/mcp/main/install.sh | sh
20
- ```
21
-
22
18
  Then register it with your MCP client. For example:
23
19
 
24
20
  ```bash
25
- claude mcp add --transport stdio bifrost -- bifrost
21
+ claude mcp add --transport stdio bifrost -- bifrost --no-auth
26
22
  ```
27
23
 
24
+ > **Note:** The `--no-auth` flag is currently recommended because there is an unresolved issue with token authentication when MCP clients launch the daemon. Without it the client has no way to read the token printed to stderr.
25
+
28
26
  The MCP client starts the daemon automatically.
29
27
 
30
28
  ## Usage
@@ -91,7 +89,8 @@ Each tab registers its own tools. Calls route to whoever owns the tool. Disconne
91
89
 
92
90
  ```bash
93
91
  git clone https://github.com/Business-Maps/mcp.git && cd mcp
94
- npm test # no install needed, no deps
92
+ npm install
93
+ npm test
95
94
  ```
96
95
 
97
96
  [Architecture](docs/architecture.md) · [Client API](docs/client-api.md) · [Contributing](CONTRIBUTING.md)
package/bin/bifrost CHANGED
@@ -83,7 +83,7 @@ function printBanner(version) {
83
83
  `${c.bold}${c.RED} ██████╔╝██║██║ ██║ ██║╚██████╔╝███████║ ██║ ${c.r}`,
84
84
  `${c.bold}${c.RED} ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ${c.r}`,
85
85
  "",
86
- `${c.dim}${c.red} ── SECURE BRIDGE PROTOCOL ──────────────── v${version} ──${c.r}`,
86
+ `${c.dim}${c.red} ---------------------------------------- v${version} --${c.r}`,
87
87
  "",
88
88
  "",
89
89
  "",
@@ -111,7 +111,7 @@ function printBanner(version) {
111
111
  // ---------------------------------------------------------------------------
112
112
  function printHelp() {
113
113
  log("");
114
- log(` ${c.bold}${c.RED}BIFROST${c.r} ${c.dim}${c.red}── SECURE BRIDGE PROTOCOL${c.r}`);
114
+ log(` ${c.bold}${c.RED}BIFROST${c.r} ${c.dim}${c.red}----------------------${c.r}`);
115
115
  log("");
116
116
  log(` ${c.dim}MCP browser bridge daemon${c.r}`);
117
117
  log("");
@@ -125,7 +125,7 @@ function printHelp() {
125
125
  log(` ${c.RED}--help${c.r}, ${c.RED}-h${c.r} Show this message`);
126
126
  log(` ${c.RED}--version${c.r}, ${c.RED}-v${c.r} Show version`);
127
127
  log("");
128
- log(` ${c.dim}${c.red}────────────────────────────────────────────────────${c.r}`);
128
+ log(` ${c.dim}${c.red}----------------------------------------------------${c.r}`);
129
129
  log("");
130
130
  }
131
131
 
@@ -356,7 +356,7 @@ const wss = new MiniWebSocketServer({ port: PORT }, () => {
356
356
  wss.on("connection", (ws, req) => {
357
357
  // Origin check — allow any localhost origin (any port)
358
358
  if (!isLocalOrigin(req.headers.origin)) {
359
- ws.close();
359
+ ws.terminate();
360
360
  return;
361
361
  }
362
362
 
@@ -366,7 +366,7 @@ wss.on("connection", (ws, req) => {
366
366
  const queryToken = url.searchParams.get("token");
367
367
  const headerToken = req.headers["sec-websocket-protocol"];
368
368
  if (queryToken !== AUTH_TOKEN && headerToken !== AUTH_TOKEN) {
369
- ws.close();
369
+ ws.terminate();
370
370
  return;
371
371
  }
372
372
  }
package/lib/websocket.js CHANGED
@@ -1,274 +1,63 @@
1
1
  // ============================================================================
2
- // Hardened Minimal WebSocket server (RFC 6455 compliant core)
3
- // Zero dependencies
2
+ // WebSocket server thin wrapper around 'ws' for API compatibility
4
3
  // ============================================================================
5
4
 
6
5
  import { createServer } from "http";
7
- import { createHash } from "crypto";
8
6
  import { EventEmitter } from "events";
9
-
10
- const GUID = "258EAFA5-E914-47DA-95CA-5AB5DC65C97B";
11
-
12
- const MAX_PAYLOAD_SIZE = 100 * 1024 * 1024; // 100MB
13
- const MAX_BUFFER_SIZE = 200 * 1024 * 1024; // 200MB total buffered
7
+ import { WebSocketServer, WebSocket } from "ws";
14
8
 
15
9
  // ---------------------------------------------------------------------------
16
- // MiniWebSocket
10
+ // MiniWebSocket — wraps a ws.WebSocket with the same API the daemon expects
17
11
  // ---------------------------------------------------------------------------
18
12
  export class MiniWebSocket extends EventEmitter {
19
- constructor(socket) {
13
+ constructor(ws) {
20
14
  super();
21
- this.socket = socket;
15
+ this._ws = ws;
22
16
  this.readyState = 1;
23
17
 
24
- this._buffer = Buffer.alloc(0);
25
-
26
- // fragmentation state
27
- this._fragType = null;
28
- this._fragBuffer = [];
29
-
30
- socket.on("data", (c) => this._onData(c));
31
- socket.on("close", () => this._handleClose());
32
- socket.on("end", () => this._handleClose());
33
- socket.on("error", (err) => {
34
- this._handleClose();
35
- this.emit("error", err);
18
+ ws.on("message", (data) => {
19
+ const text = typeof data === "string" ? data : data.toString();
20
+ this.emit("message", text);
36
21
  });
37
- }
38
-
39
- // -------------------------------------------------------------------------
40
- // Public API
41
- // -------------------------------------------------------------------------
42
- send(data) {
43
- if (this.readyState !== 1) return;
44
-
45
- const payload = Buffer.isBuffer(data)
46
- ? data
47
- : Buffer.from(String(data));
48
-
49
- this._writeFrame(0x1, payload);
50
- }
51
-
52
- close(code = 1000) {
53
- if (this.readyState !== 1) return;
54
- this.readyState = 2;
55
-
56
- const buf = Buffer.alloc(2);
57
- buf.writeUInt16BE(code, 0);
58
-
59
- this._writeFrame(0x8, buf);
60
- this.socket.end();
61
- }
62
-
63
- // -------------------------------------------------------------------------
64
- // Core frame processing
65
- // -------------------------------------------------------------------------
66
- _onData(chunk) {
67
- this._buffer = Buffer.concat([this._buffer, chunk]);
68
-
69
- if (this._buffer.length > MAX_BUFFER_SIZE) {
70
- this._fail(1009, "buffer overflow");
71
- return;
72
- }
73
-
74
- while (true) {
75
- const frame = this._decodeFrame(this._buffer);
76
- if (!frame) break;
77
-
78
- if (frame.error) {
79
- this._fail(frame.code || 1002, frame.error);
80
- return;
81
- }
82
-
83
- this._buffer = this._buffer.subarray(frame.totalLength);
84
- this._handleFrame(frame);
85
- }
86
- }
87
-
88
- _handleFrame(f) {
89
- const { opcode, payload, fin } = f;
90
-
91
- // control frames
92
- if (opcode === 0x8) {
93
- // close
94
- this.close();
95
- return;
96
- }
97
-
98
- if (opcode === 0x9) {
99
- // ping → pong
100
- this._writeFrame(0xA, payload);
101
- return;
102
- }
103
-
104
- if (opcode === 0xA) {
105
- this.emit("pong");
106
- return;
107
- }
108
22
 
109
- // fragmentation handling
110
- if (opcode === 0x0) {
111
- if (!this._fragType) return this._fail(1002, "unexpected continuation");
112
-
113
- this._fragBuffer.push(payload);
114
-
115
- if (fin) {
116
- const full = Buffer.concat(this._fragBuffer);
117
- this._emitMessage(this._fragType, full);
118
- this._fragType = null;
119
- this._fragBuffer = [];
120
- }
121
- return;
122
- }
123
-
124
- if (opcode === 0x1 || opcode === 0x2) {
125
- if (this._fragType) return this._fail(1002, "nested fragments");
23
+ ws.on("close", () => {
24
+ if (this.readyState === 3) return;
25
+ this.readyState = 3;
26
+ this.emit("close");
27
+ });
126
28
 
127
- if (!fin) {
128
- this._fragType = opcode;
129
- this._fragBuffer = [payload];
130
- return;
29
+ ws.on("error", (err) => {
30
+ if (this.readyState !== 3) {
31
+ this.readyState = 3;
32
+ this.emit("close");
131
33
  }
34
+ this.emit("error", err);
35
+ });
132
36
 
133
- this._emitMessage(opcode, payload);
134
- return;
135
- }
136
-
137
- this._fail(1002, "invalid opcode");
138
- }
139
-
140
- _emitMessage(opcode, payload) {
141
- if (opcode === 0x1) {
142
- // text
143
- try {
144
- const text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
145
- this.emit("message", text);
146
- } catch {
147
- this._fail(1007, "invalid utf8");
148
- }
149
- } else {
150
- // binary
151
- this.emit("message", payload);
152
- }
37
+ ws.on("pong", () => this.emit("pong"));
153
38
  }
154
39
 
155
- // -------------------------------------------------------------------------
156
- // Frame decode (STRICT)
157
- // -------------------------------------------------------------------------
158
- _decodeFrame(buf) {
159
- if (buf.length < 2) return null;
160
-
161
- const b0 = buf[0];
162
- const b1 = buf[1];
163
-
164
- const fin = (b0 & 0x80) !== 0;
165
- const opcode = b0 & 0x0f;
166
-
167
- const masked = (b1 & 0x80) !== 0;
168
- let payloadLen = b1 & 0x7f;
169
-
170
- // MUST be masked (client → server)
171
- if (!masked) return { error: "unmasked frame", code: 1002 };
172
-
173
- // opcode validation
174
- const valid = [0x0, 0x1, 0x2, 0x8, 0x9, 0xA];
175
- if (!valid.includes(opcode)) {
176
- return { error: "invalid opcode", code: 1002 };
177
- }
178
-
179
- let offset = 2;
180
-
181
- if (payloadLen === 126) {
182
- if (buf.length < 4) return null;
183
- payloadLen = buf.readUInt16BE(2);
184
- offset = 4;
185
- } else if (payloadLen === 127) {
186
- if (buf.length < 10) return null;
187
- const big = buf.readBigUInt64BE(2);
188
- if (big > BigInt(MAX_PAYLOAD_SIZE)) {
189
- return { error: "too large", code: 1009 };
190
- }
191
- payloadLen = Number(big);
192
- offset = 10;
193
- }
194
-
195
- if (payloadLen > MAX_PAYLOAD_SIZE) {
196
- return { error: "too large", code: 1009 };
197
- }
198
-
199
- // control frames rules
200
- if (opcode >= 0x8) {
201
- if (!fin) return { error: "fragmented control", code: 1002 };
202
- if (payloadLen > 125) return { error: "control too large", code: 1002 };
203
- }
204
-
205
- const total = offset + 4 + payloadLen;
206
- if (buf.length < total) return null;
207
-
208
- const mask = buf.subarray(offset, offset + 4);
209
- const payload = Buffer.alloc(payloadLen);
210
-
211
- for (let i = 0; i < payloadLen; i++) {
212
- payload[i] = buf[offset + 4 + i] ^ mask[i % 4];
213
- }
40
+ get socket() { return this._ws._socket || null; }
41
+ get bufferedAmount() { return this._ws.bufferedAmount; }
214
42
 
215
- return {
216
- fin,
217
- opcode,
218
- payload,
219
- totalLength: total,
220
- };
43
+ send(data) {
44
+ if (this._ws.readyState !== WebSocket.OPEN) return;
45
+ this._ws.send(typeof data === "string" ? data : String(data));
221
46
  }
222
47
 
223
- // -------------------------------------------------------------------------
224
- // Frame encode
225
- // -------------------------------------------------------------------------
226
- _writeFrame(opcode, payload) {
227
- if (this.readyState !== 1) return;
228
-
229
- const len = payload.length;
230
- let header;
231
-
232
- if (len < 126) {
233
- header = Buffer.alloc(2);
234
- header[1] = len;
235
- } else if (len < 65536) {
236
- header = Buffer.alloc(4);
237
- header[1] = 126;
238
- header.writeUInt16BE(len, 2);
239
- } else {
240
- header = Buffer.alloc(10);
241
- header[1] = 127;
242
- header.writeBigUInt64BE(BigInt(len), 2);
243
- }
244
-
245
- header[0] = 0x80 | opcode;
246
-
247
- const frame = Buffer.concat([header, payload]);
248
-
249
- if (!this.socket.write(frame)) {
250
- this.socket.once("drain", () => {});
48
+ close(code = 1000) {
49
+ if (this._ws.readyState === WebSocket.OPEN || this._ws.readyState === WebSocket.CONNECTING) {
50
+ this._ws.close(code);
251
51
  }
252
52
  }
253
53
 
254
- _fail(code, reason) {
255
- try {
256
- const buf = Buffer.alloc(2);
257
- buf.writeUInt16BE(code, 0);
258
- this._writeFrame(0x8, buf);
259
- } catch {}
260
- this.socket.destroy();
261
- }
262
-
263
- _handleClose() {
264
- if (this.readyState === 3) return;
265
- this.readyState = 3;
266
- this.emit("close");
54
+ terminate() {
55
+ this._ws.terminate();
267
56
  }
268
57
  }
269
58
 
270
59
  // ---------------------------------------------------------------------------
271
- // Server
60
+ // MiniWebSocketServer — wraps ws.WebSocketServer, emits MiniWebSocket
272
61
  // ---------------------------------------------------------------------------
273
62
  export class MiniWebSocketServer extends EventEmitter {
274
63
  constructor({ port }, cb) {
@@ -279,51 +68,20 @@ export class MiniWebSocketServer extends EventEmitter {
279
68
  res.end("Upgrade required");
280
69
  });
281
70
 
282
- this.server.on("upgrade", (req, socket, head) => {
283
- // strict handshake validation
284
- if (
285
- req.headers["upgrade"]?.toLowerCase() !== "websocket" ||
286
- !req.headers["connection"]?.toLowerCase().includes("upgrade") ||
287
- req.headers["sec-websocket-version"] !== "13"
288
- ) {
289
- socket.destroy();
290
- return;
291
- }
292
-
293
- const key = req.headers["sec-websocket-key"];
294
- if (!key) {
295
- socket.destroy();
296
- return;
297
- }
298
-
299
- const accept = createHash("sha1")
300
- .update(key + GUID)
301
- .digest("base64");
302
-
303
- // subprotocol (used for auth in your daemon)
304
- const protocol = req.headers["sec-websocket-protocol"];
305
-
306
- socket.setNoDelay(true);
307
- socket.setTimeout(0);
308
-
309
- socket.write(
310
- "HTTP/1.1 101 Switching Protocols\r\n" +
311
- "Upgrade: websocket\r\n" +
312
- "Connection: Upgrade\r\n" +
313
- `Sec-WebSocket-Accept: ${accept}\r\n` +
314
- (protocol ? `Sec-WebSocket-Protocol: ${protocol}\r\n` : "") +
315
- "\r\n"
316
- );
317
-
318
- if (head?.length) socket.unshift(head);
71
+ this._wss = new WebSocketServer({
72
+ server: this.server,
73
+ perMessageDeflate: false,
74
+ });
319
75
 
320
- this.emit("connection", new MiniWebSocket(socket), req);
76
+ this._wss.on("connection", (ws, req) => {
77
+ this.emit("connection", new MiniWebSocket(ws), req);
321
78
  });
322
79
 
323
80
  this.server.listen(port, "127.0.0.1", cb);
324
81
  }
325
82
 
326
83
  close(cb) {
84
+ this._wss.close();
327
85
  this.server.close(cb);
328
86
  }
329
- }
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@businessmaps/bifrost",
3
- "version": "0.0.2",
3
+ "version": "0.1.2",
4
4
  "description": "Bridge any browser web app to Claude Code via MCP",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,5 +35,8 @@
35
35
  "lib/",
36
36
  "LICENSE",
37
37
  "README.md"
38
- ]
38
+ ],
39
+ "dependencies": {
40
+ "ws": "^8.20.0"
41
+ }
39
42
  }