@jobshimo/browser-link 0.2.0 → 0.4.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.
Files changed (60) hide show
  1. package/README.md +37 -0
  2. package/dist/bridge/client.d.ts +101 -0
  3. package/dist/bridge/client.js +435 -0
  4. package/dist/bridge/client.js.map +1 -0
  5. package/dist/bridge/dispatch.d.ts +29 -0
  6. package/dist/bridge/dispatch.js +39 -0
  7. package/dist/bridge/dispatch.js.map +1 -0
  8. package/dist/bridge/events.d.ts +39 -0
  9. package/dist/bridge/events.js +47 -0
  10. package/dist/bridge/events.js.map +1 -0
  11. package/dist/bridge/protocol.d.ts +80 -0
  12. package/dist/bridge/protocol.js +79 -0
  13. package/dist/bridge/protocol.js.map +1 -0
  14. package/dist/bridge/server.d.ts +42 -0
  15. package/dist/bridge/server.js +336 -0
  16. package/dist/bridge/server.js.map +1 -0
  17. package/dist/bridge/token.d.ts +17 -0
  18. package/dist/bridge/token.js +79 -0
  19. package/dist/bridge/token.js.map +1 -0
  20. package/dist/cli.js +94 -15
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/doctor.d.ts +12 -1
  23. package/dist/commands/doctor.js +90 -20
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/extension.d.ts +3 -2
  26. package/dist/commands/extension.js +53 -28
  27. package/dist/commands/extension.js.map +1 -1
  28. package/dist/commands/multi-agent.d.ts +7 -0
  29. package/dist/commands/multi-agent.js +109 -0
  30. package/dist/commands/multi-agent.js.map +1 -0
  31. package/dist/commands/tools.d.ts +11 -0
  32. package/dist/commands/tools.js +168 -0
  33. package/dist/commands/tools.js.map +1 -0
  34. package/dist/commands/updates.d.ts +2 -1
  35. package/dist/commands/updates.js +24 -7
  36. package/dist/commands/updates.js.map +1 -1
  37. package/dist/config.d.ts +25 -3
  38. package/dist/config.js +35 -2
  39. package/dist/config.js.map +1 -1
  40. package/dist/messages.d.ts +7 -0
  41. package/dist/permissions.d.ts +37 -0
  42. package/dist/permissions.js +156 -0
  43. package/dist/permissions.js.map +1 -0
  44. package/dist/server.d.ts +7 -3
  45. package/dist/server.js +160 -32
  46. package/dist/server.js.map +1 -1
  47. package/dist/tools/browser-definitions.js +18 -0
  48. package/dist/tools/browser-definitions.js.map +1 -1
  49. package/dist/tools/browser-dispatch.d.ts +7 -0
  50. package/dist/tools/browser-dispatch.js +11 -0
  51. package/dist/tools/browser-dispatch.js.map +1 -1
  52. package/dist/tools/server-instructions.d.ts +1 -1
  53. package/dist/tools/server-instructions.js +4 -0
  54. package/dist/tools/server-instructions.js.map +1 -1
  55. package/dist/ui/app.js +16 -1
  56. package/dist/ui/app.js.map +1 -1
  57. package/dist/ui/screens.d.ts +14 -1
  58. package/dist/ui/screens.js +314 -2
  59. package/dist/ui/screens.js.map +1 -1
  60. package/package.json +4 -3
package/README.md CHANGED
@@ -52,9 +52,46 @@ browser-link install --client copilot # register only in GitHub Copilot CLI
52
52
  browser-link uninstall --client opencode # remove from one client
53
53
  browser-link extension # show the Chrome extension assets path + steps
54
54
  browser-link doctor # diagnose current setup
55
+ browser-link tools # show which MCP tools are enabled / disabled
56
+ browser-link tools disable browser.evaluate
57
+ browser-link tools preset readonly # all | readonly | no-eval | no-map
58
+ browser-link updates # check the npm registry for a newer version
55
59
  browser-link about # what this is, how it works, every tool
56
60
  ```
57
61
 
62
+ ## Per-tool permissions
63
+
64
+ `browser-link` exposes 17 MCP tools by default — 10 browser-bridge tools,
65
+ 6 UI-map tools, and `browser.events` for bridge traceability. You can
66
+ disable any subset per machine, either through the **Permissions** screen
67
+ in the interactive menu (toggle with Space, apply a preset with Enter,
68
+ save with `s`) or through the scriptable `browser-link tools` subcommand.
69
+ Available presets: `all` (default), `readonly`, `no-eval`, `no-map`.
70
+ Changes take effect the next time your MCP client starts the server.
71
+
72
+ ## Multi-agent mode
73
+
74
+ By default only one MCP client can have browser-link active at a time
75
+ (EADDRINUSE on the second). Enable multi-agent mode and the second
76
+ `browser-link` spawn becomes a proxy that forwards MCP requests to the
77
+ first via `127.0.0.1:17530`, with the same kernel-level process binding
78
+ the WS port already uses. All connected clients share the same Chrome
79
+ tabs and persistent UI map.
80
+
81
+ ```bash
82
+ browser-link multi-agent enable
83
+ browser-link multi-agent auto-reelect enable # optional
84
+ ```
85
+
86
+ With `auto-reelect` on, secondary proxies survive the primary closing:
87
+ they enter a 5-second reconnect window, return `-32001 "temporarily
88
+ unavailable"` for in-flight requests, and hot-swap to the fresh primary
89
+ once it appears. The agent self-recovers from stale tab ids via the new
90
+ `browser.events` tool, which surfaces a ring buffer of bridge lifecycle
91
+ events (`primary-elected`, `tab-registered`, `tab-disconnected`,
92
+ `tab-renamed`). The Chrome extension preserves the per-tab id across
93
+ primary swaps via `chrome.storage.session`.
94
+
58
95
  After `install`, restart the MCP client so it picks up the new entry.
59
96
  After `extension`, follow the printed steps to load the unpacked extension
60
97
  in Chrome. Then click "Conectar" on every tab you want the agent to reach
@@ -0,0 +1,101 @@
1
+ import { type Interface } from 'node:readline';
2
+ /** Raised when the proxy cannot complete the IPC handshake. */
3
+ export declare class HandshakeError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface ConnectOptions {
7
+ host?: string;
8
+ port?: number;
9
+ /** How long to wait for a hello-ack after sending the hello. */
10
+ handshakeTimeoutMs?: number;
11
+ }
12
+ export interface ConnectionInfo {
13
+ sessionId: string;
14
+ version: string;
15
+ }
16
+ /** Minimal IPC client. Owns the TCP socket, handles framing, runs the
17
+ * handshake, dispatches mcp.response frames back to whoever sent the
18
+ * matching mcp.request. */
19
+ export declare class IpcClient {
20
+ private socket;
21
+ private buffer;
22
+ private nextRequestId;
23
+ private pendingRequests;
24
+ private closeListeners;
25
+ private notificationListeners;
26
+ private closed;
27
+ /** Open the TCP connection, perform the handshake, and resolve with the
28
+ * session info from the primary's hello-ack. On any handshake failure,
29
+ * rejects with HandshakeError and tears down the socket. */
30
+ connect(token: string, opts?: ConnectOptions): Promise<ConnectionInfo>;
31
+ private handshakeReceiver;
32
+ /** Forward a JSON-RPC request as an mcp.request frame. Resolves with the
33
+ * primary's JSON-RPC response payload. Rejects if the socket closes. */
34
+ sendMcpRequest(jsonRpcPayload: unknown): Promise<unknown>;
35
+ /** Forward a JSON-RPC notification as an mcp.notification frame. Fire-
36
+ * and-forget — notifications have no response in JSON-RPC. */
37
+ sendMcpNotification(jsonRpcPayload: unknown): void;
38
+ /** Register a callback invoked when the IPC connection drops. The
39
+ * `reason` argument distinguishes a primary-closing broadcast from a
40
+ * plain remote close so callers can decide to re-elect vs exit. */
41
+ onClose(cb: (reason: 'remote' | 'local' | 'primary-closing') => void): void;
42
+ /** Register a callback for unsolicited notifications from the primary
43
+ * (e.g. tools/list_changed in the future). */
44
+ onNotification(cb: (payload: unknown) => void): void;
45
+ /** Close the IPC connection. */
46
+ disconnect(): Promise<void>;
47
+ private ingest;
48
+ private handleFrame;
49
+ private onSocketClose;
50
+ private notifyClose;
51
+ }
52
+ export interface ProxyOptions {
53
+ input?: NodeJS.ReadableStream;
54
+ output?: NodeJS.WritableStream;
55
+ /** Called when the IPC connection drops and (if reelect was enabled)
56
+ * could not be re-established. After this fires the proxy is dead;
57
+ * callers usually want to exit the process. */
58
+ onClose?: (reason: 'remote' | 'local' | 'primary-closing') => void;
59
+ /** Override the token used in the hello. Tests pass this directly so
60
+ * they don't need to write a token file. Production reads it from disk. */
61
+ token?: string;
62
+ /** Override IPC endpoint. */
63
+ host?: string;
64
+ port?: number;
65
+ /** When true, instead of giving up on the first IPC drop, the proxy
66
+ * tries to reconnect to a fresh primary at the same IPC port for up to
67
+ * reelectTimeoutMs. During that window, any incoming JSON-RPC request
68
+ * from stdin gets an immediate "bridge unavailable" error response so
69
+ * the MCP client doesn't hang. Default: false. */
70
+ autoReelect?: boolean;
71
+ /** Total budget (ms) to wait for a new primary before giving up.
72
+ * Default: 5000. */
73
+ reelectTimeoutMs?: number;
74
+ /** Per-attempt interval (ms) between reconnect tries. Default: 200. */
75
+ reelectIntervalMs?: number;
76
+ /** Hook for tests: called whenever the proxy starts a reconnect
77
+ * attempt cycle. Production callers ignore. */
78
+ onReelectStart?: () => void;
79
+ /** Hook for tests: called when a reconnect cycle succeeds. */
80
+ onReelectSuccess?: () => void;
81
+ /** Hook for tests: called when a reconnect cycle exhausts the budget. */
82
+ onReelectExhausted?: () => void;
83
+ }
84
+ export interface ProxyHandle {
85
+ client: IpcClient;
86
+ stop(): Promise<void>;
87
+ /** Resolves once the IPC connection drops (any reason). */
88
+ closed: Promise<void>;
89
+ }
90
+ /** Read the token from disk and connect to the running primary, then plug
91
+ * stdin → IPC mcp.request and IPC mcp.response → stdout.
92
+ *
93
+ * When `autoReelect: true` is passed, the proxy survives IPC drops by
94
+ * waiting for a fresh primary to appear at the same port. During the
95
+ * wait, incoming JSON-RPC requests get an immediate error response so
96
+ * the MCP client never hangs on a missing reply. */
97
+ export declare function runProxy(opts?: ProxyOptions): Promise<ProxyHandle>;
98
+ /** Convenience: open a readline-style line iterator over a stream. Currently
99
+ * unused (we use a hand-rolled line buffer for symmetry with the server)
100
+ * but exported for future tooling. */
101
+ export declare function readlines(stream: NodeJS.ReadableStream): Interface;
@@ -0,0 +1,435 @@
1
+ import { connect } from 'node:net';
2
+ import { createInterface } from 'node:readline';
3
+ import { IPC_HOST, IPC_PORT, IPC_PROTOCOL_VERSION, encodeFrame, parseFrame, } from './protocol.js';
4
+ import { readToken } from './token.js';
5
+ /**
6
+ * Proxy's side of the IPC bridge. The proxy is a thin browser-link process
7
+ * spawned by an MCP client (Claude / Copilot / OpenCode) that finds the WS
8
+ * port already taken — it forwards every MCP frame it gets on stdin to the
9
+ * primary via the IPC socket, and pipes responses back to stdout.
10
+ *
11
+ * Two flavours:
12
+ * - IpcClient is the bare connection (used by tests and re-election).
13
+ * - runProxy() wires IpcClient to stdin/stdout so the MCP client thinks
14
+ * it is talking to a real server.
15
+ */
16
+ function log(msg) {
17
+ console.error(`[browser-link proxy] ${msg}`);
18
+ }
19
+ /** Raised when the proxy cannot complete the IPC handshake. */
20
+ export class HandshakeError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'HandshakeError';
24
+ }
25
+ }
26
+ /** Minimal IPC client. Owns the TCP socket, handles framing, runs the
27
+ * handshake, dispatches mcp.response frames back to whoever sent the
28
+ * matching mcp.request. */
29
+ export class IpcClient {
30
+ socket = null;
31
+ buffer = '';
32
+ nextRequestId = 1;
33
+ pendingRequests = new Map();
34
+ closeListeners = [];
35
+ notificationListeners = [];
36
+ closed = false;
37
+ /** Open the TCP connection, perform the handshake, and resolve with the
38
+ * session info from the primary's hello-ack. On any handshake failure,
39
+ * rejects with HandshakeError and tears down the socket. */
40
+ async connect(token, opts = {}) {
41
+ const host = opts.host ?? IPC_HOST;
42
+ const port = opts.port ?? IPC_PORT;
43
+ const handshakeTimeoutMs = opts.handshakeTimeoutMs ?? 4000;
44
+ const socket = await new Promise((resolve, reject) => {
45
+ const s = connect({ host, port });
46
+ s.once('connect', () => resolve(s));
47
+ s.once('error', (err) => reject(err));
48
+ });
49
+ this.socket = socket;
50
+ socket.on('data', (chunk) => this.ingest(chunk));
51
+ socket.on('close', () => this.onSocketClose('remote'));
52
+ socket.on('error', (err) => log(`Socket error: ${err.message}`));
53
+ // Send hello, wait for ack or reject or timeout.
54
+ const ack = await new Promise((resolve, reject) => {
55
+ const timer = setTimeout(() => {
56
+ reject(new HandshakeError('Handshake timed out — primary did not reply.'));
57
+ }, handshakeTimeoutMs);
58
+ const onFirstFrame = (frame) => {
59
+ clearTimeout(timer);
60
+ if (frame.kind === 'hello-ack') {
61
+ resolve({ sessionId: frame.sessionId, version: frame.version });
62
+ return;
63
+ }
64
+ if (frame.kind === 'hello-reject') {
65
+ reject(new HandshakeError(`Primary rejected: ${frame.reason}`));
66
+ return;
67
+ }
68
+ reject(new HandshakeError(`Unexpected first frame: ${frame.kind}`));
69
+ };
70
+ this.handshakeReceiver = onFirstFrame;
71
+ try {
72
+ socket.write(encodeFrame({ kind: 'hello', version: IPC_PROTOCOL_VERSION, token }));
73
+ }
74
+ catch (err) {
75
+ clearTimeout(timer);
76
+ reject(err instanceof Error ? err : new Error(String(err)));
77
+ }
78
+ }).catch((err) => {
79
+ this.disconnect().catch(() => {
80
+ /* ignore */
81
+ });
82
+ throw err;
83
+ });
84
+ this.handshakeReceiver = null;
85
+ log(`Handshake ok — session=${ack.sessionId}`);
86
+ return ack;
87
+ }
88
+ handshakeReceiver = null;
89
+ /** Forward a JSON-RPC request as an mcp.request frame. Resolves with the
90
+ * primary's JSON-RPC response payload. Rejects if the socket closes. */
91
+ sendMcpRequest(jsonRpcPayload) {
92
+ if (!this.socket || this.closed) {
93
+ return Promise.reject(new Error('Proxy is not connected.'));
94
+ }
95
+ const requestId = this.nextRequestId++;
96
+ return new Promise((resolve, reject) => {
97
+ this.pendingRequests.set(requestId, resolve);
98
+ const onCloseHandler = () => {
99
+ if (this.pendingRequests.delete(requestId)) {
100
+ reject(new Error('Primary connection closed while a request was in flight.'));
101
+ }
102
+ };
103
+ this.onClose(onCloseHandler);
104
+ try {
105
+ this.socket.write(encodeFrame({ kind: 'mcp.request', requestId, payload: jsonRpcPayload }));
106
+ }
107
+ catch (err) {
108
+ this.pendingRequests.delete(requestId);
109
+ reject(err instanceof Error ? err : new Error(String(err)));
110
+ }
111
+ });
112
+ }
113
+ /** Forward a JSON-RPC notification as an mcp.notification frame. Fire-
114
+ * and-forget — notifications have no response in JSON-RPC. */
115
+ sendMcpNotification(jsonRpcPayload) {
116
+ if (!this.socket || this.closed)
117
+ return;
118
+ try {
119
+ this.socket.write(encodeFrame({ kind: 'mcp.notification', payload: jsonRpcPayload }));
120
+ }
121
+ catch {
122
+ /* socket gone; close handler will fire */
123
+ }
124
+ }
125
+ /** Register a callback invoked when the IPC connection drops. The
126
+ * `reason` argument distinguishes a primary-closing broadcast from a
127
+ * plain remote close so callers can decide to re-elect vs exit. */
128
+ onClose(cb) {
129
+ this.closeListeners.push(cb);
130
+ }
131
+ /** Register a callback for unsolicited notifications from the primary
132
+ * (e.g. tools/list_changed in the future). */
133
+ onNotification(cb) {
134
+ this.notificationListeners.push(cb);
135
+ }
136
+ /** Close the IPC connection. */
137
+ async disconnect() {
138
+ if (this.closed)
139
+ return;
140
+ this.closed = true;
141
+ if (this.socket) {
142
+ const s = this.socket;
143
+ this.socket = null;
144
+ try {
145
+ s.end();
146
+ }
147
+ catch {
148
+ /* ignore */
149
+ }
150
+ s.destroy();
151
+ // Fire close listeners with reason=local (we initiated it).
152
+ this.notifyClose('local');
153
+ }
154
+ }
155
+ ingest(chunk) {
156
+ this.buffer += chunk.toString('utf8');
157
+ let nl;
158
+ while ((nl = this.buffer.indexOf('\n')) >= 0) {
159
+ const line = this.buffer.slice(0, nl);
160
+ this.buffer = this.buffer.slice(nl + 1);
161
+ if (line.length === 0)
162
+ continue;
163
+ const frame = parseFrame(line);
164
+ if (!frame) {
165
+ log('Invalid frame from primary; dropping.');
166
+ continue;
167
+ }
168
+ this.handleFrame(frame);
169
+ }
170
+ }
171
+ handleFrame(frame) {
172
+ if (this.handshakeReceiver) {
173
+ this.handshakeReceiver(frame);
174
+ return;
175
+ }
176
+ switch (frame.kind) {
177
+ case 'mcp.response': {
178
+ const resolve = this.pendingRequests.get(frame.requestId);
179
+ if (resolve) {
180
+ this.pendingRequests.delete(frame.requestId);
181
+ resolve(frame.payload);
182
+ }
183
+ return;
184
+ }
185
+ case 'mcp.notification': {
186
+ for (const cb of this.notificationListeners)
187
+ cb(frame.payload);
188
+ return;
189
+ }
190
+ case 'ping': {
191
+ try {
192
+ this.socket?.write(encodeFrame({ kind: 'pong' }));
193
+ }
194
+ catch {
195
+ /* socket gone */
196
+ }
197
+ return;
198
+ }
199
+ case 'pong': {
200
+ // Future: we can send our own pings if needed. Today the primary
201
+ // initiates the heartbeat; pong from us → primary.
202
+ return;
203
+ }
204
+ case 'primary-closing': {
205
+ log(`Primary signalled close (${frame.reason ?? 'no reason'}).`);
206
+ this.notifyClose('primary-closing');
207
+ return;
208
+ }
209
+ default:
210
+ return;
211
+ }
212
+ }
213
+ onSocketClose(reason) {
214
+ if (this.closed)
215
+ return;
216
+ this.closed = true;
217
+ log('Socket closed by primary.');
218
+ this.notifyClose(reason);
219
+ }
220
+ notifyClose(reason) {
221
+ // Reject every in-flight request first so callers don't hang.
222
+ for (const resolve of this.pendingRequests.values()) {
223
+ // We resolve with a JSON-RPC error envelope so the MCP client gets
224
+ // a tool error instead of a stuck promise.
225
+ resolve({
226
+ jsonrpc: '2.0',
227
+ id: null,
228
+ error: { code: -32000, message: 'browser-link primary disconnected.' },
229
+ });
230
+ }
231
+ this.pendingRequests.clear();
232
+ for (const cb of this.closeListeners.splice(0)) {
233
+ try {
234
+ cb(reason);
235
+ }
236
+ catch (err) {
237
+ log(`Close listener threw: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ /** Read the token from disk and connect to the running primary, then plug
243
+ * stdin → IPC mcp.request and IPC mcp.response → stdout.
244
+ *
245
+ * When `autoReelect: true` is passed, the proxy survives IPC drops by
246
+ * waiting for a fresh primary to appear at the same port. During the
247
+ * wait, incoming JSON-RPC requests get an immediate error response so
248
+ * the MCP client never hangs on a missing reply. */
249
+ export async function runProxy(opts = {}) {
250
+ const input = opts.input ?? process.stdin;
251
+ const output = opts.output ?? process.stdout;
252
+ const autoReelect = opts.autoReelect === true;
253
+ const reelectTimeoutMs = opts.reelectTimeoutMs ?? 5000;
254
+ const reelectIntervalMs = opts.reelectIntervalMs ?? 200;
255
+ const initialToken = opts.token ?? readToken();
256
+ if (!initialToken) {
257
+ throw new Error('Multi-agent token not found. The primary browser-link instance is not running with multi-agent enabled.');
258
+ }
259
+ let client = new IpcClient();
260
+ await client.connect(initialToken, { host: opts.host, port: opts.port });
261
+ /* When the IPC drops AND autoReelect is on, we enter "reconnecting"
262
+ * mode. While reconnecting, the data pipe stays attached but requests
263
+ * are answered immediately with an error envelope so the MCP client
264
+ * does not stall on a missing reply. */
265
+ let reconnecting = false;
266
+ let stopped = false;
267
+ let closedResolve = () => { };
268
+ const closed = new Promise((resolve) => {
269
+ closedResolve = resolve;
270
+ });
271
+ const fail = (reason) => {
272
+ if (stopped)
273
+ return;
274
+ stopped = true;
275
+ input.off?.('data', onData);
276
+ if (opts.onClose) {
277
+ try {
278
+ opts.onClose(reason);
279
+ }
280
+ catch (err) {
281
+ log(`onClose handler threw: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
284
+ closedResolve();
285
+ };
286
+ // Set up the input pipe. We can't use readline.createInterface on a raw
287
+ // stream that has already been data-event-attached, but we can use a
288
+ // simple line buffer like the server side.
289
+ let lineBuffer = '';
290
+ const onData = (chunk) => {
291
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
292
+ lineBuffer += text;
293
+ let nl;
294
+ while ((nl = lineBuffer.indexOf('\n')) >= 0) {
295
+ const line = lineBuffer.slice(0, nl).replace(/\r$/, '');
296
+ lineBuffer = lineBuffer.slice(nl + 1);
297
+ if (line.length === 0)
298
+ continue;
299
+ handleLine(line);
300
+ }
301
+ };
302
+ const handleLine = (line) => {
303
+ let msg;
304
+ try {
305
+ msg = JSON.parse(line);
306
+ }
307
+ catch {
308
+ output.write(JSON.stringify({
309
+ jsonrpc: '2.0',
310
+ id: null,
311
+ error: { code: -32700, message: 'Parse error in proxy input.' },
312
+ }) + '\n');
313
+ return;
314
+ }
315
+ if (!msg || typeof msg !== 'object')
316
+ return;
317
+ if (msg.id === undefined || msg.id === null) {
318
+ // Notification — fire and forget when connected; drop during reconnect.
319
+ if (!reconnecting)
320
+ client.sendMcpNotification(msg);
321
+ return;
322
+ }
323
+ if (reconnecting) {
324
+ // Fail fast so the MCP client can decide to retry.
325
+ output.write(JSON.stringify({
326
+ jsonrpc: '2.0',
327
+ id: msg.id,
328
+ error: {
329
+ code: -32001,
330
+ message: 'browser-link bridge temporarily unavailable (primary just closed; reconnecting).',
331
+ },
332
+ }) + '\n');
333
+ return;
334
+ }
335
+ client
336
+ .sendMcpRequest(msg)
337
+ .then((responsePayload) => {
338
+ output.write(JSON.stringify(responsePayload) + '\n');
339
+ })
340
+ .catch((err) => {
341
+ output.write(JSON.stringify({
342
+ jsonrpc: '2.0',
343
+ id: msg.id,
344
+ error: { code: -32000, message: err instanceof Error ? err.message : String(err) },
345
+ }) + '\n');
346
+ });
347
+ };
348
+ input.on('data', onData);
349
+ const wireCloseHandler = (c) => {
350
+ c.onClose(async (reason) => {
351
+ if (stopped)
352
+ return;
353
+ if (!autoReelect) {
354
+ fail(reason);
355
+ return;
356
+ }
357
+ // Enter reconnect mode and try to find a new primary.
358
+ reconnecting = true;
359
+ opts.onReelectStart?.();
360
+ log(`Primary closed (${reason}); reelect window opened for ${reelectTimeoutMs}ms.`);
361
+ const newClient = await reconnectLoop(opts.host, opts.port, reelectTimeoutMs, reelectIntervalMs);
362
+ if (stopped) {
363
+ if (newClient)
364
+ await newClient.disconnect();
365
+ return;
366
+ }
367
+ if (!newClient) {
368
+ log('Reelect window exhausted; closing proxy.');
369
+ opts.onReelectExhausted?.();
370
+ reconnecting = false;
371
+ fail(reason);
372
+ return;
373
+ }
374
+ // Hot-swap clients. The old one is already torn down by its close
375
+ // handler chain; we just install the new one and wire it up.
376
+ log('Reconnected to new primary.');
377
+ opts.onReelectSuccess?.();
378
+ client = newClient;
379
+ reconnecting = false;
380
+ wireCloseHandler(client);
381
+ });
382
+ };
383
+ wireCloseHandler(client);
384
+ const stop = async () => {
385
+ stopped = true;
386
+ input.off?.('data', onData);
387
+ await client.disconnect();
388
+ closedResolve();
389
+ };
390
+ return {
391
+ get client() {
392
+ return client;
393
+ },
394
+ stop,
395
+ closed,
396
+ };
397
+ }
398
+ /** Repeatedly try to connect to the IPC port until success or budget
399
+ * expires. Re-reads the token on every attempt because a new primary
400
+ * rotates it on startup. Returns the connected client, or null on
401
+ * timeout. */
402
+ async function reconnectLoop(host, port, totalBudgetMs, intervalMs) {
403
+ const deadline = Date.now() + totalBudgetMs;
404
+ // Generous handshake budget — Windows under load can take >200ms for a
405
+ // local socket round-trip and a too-tight ceiling makes reconnects flap.
406
+ const handshakeTimeoutMs = Math.min(1500, Math.max(800, intervalMs * 4));
407
+ while (Date.now() < deadline) {
408
+ await new Promise((r) => setTimeout(r, intervalMs));
409
+ const token = readToken();
410
+ if (!token)
411
+ continue;
412
+ const c = new IpcClient();
413
+ try {
414
+ await c.connect(token, { host, port, handshakeTimeoutMs });
415
+ return c;
416
+ }
417
+ catch {
418
+ try {
419
+ await c.disconnect();
420
+ }
421
+ catch {
422
+ /* ignore */
423
+ }
424
+ // Retry until deadline.
425
+ }
426
+ }
427
+ return null;
428
+ }
429
+ /** Convenience: open a readline-style line iterator over a stream. Currently
430
+ * unused (we use a hand-rolled line buffer for symmetry with the server)
431
+ * but exported for future tooling. */
432
+ export function readlines(stream) {
433
+ return createInterface({ input: stream, crlfDelay: Infinity });
434
+ }
435
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/bridge/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pure MCP request handlers — used by both the SDK Server (stdio transport)
3
+ * and the IPC bridge (TCP transport for proxies). Lives in `bridge/` because
4
+ * sharing it with the IPC server was the reason to extract these out of
5
+ * the monolithic server.ts startup.
6
+ *
7
+ * No I/O, no transport, no SDK types — just request data in, response data
8
+ * out. Both callers wrap these results in whatever envelope they need.
9
+ */
10
+ import { type BrowserToolDeps } from '../tools/browser-dispatch.js';
11
+ export interface DispatchDeps {
12
+ browserTools: BrowserToolDeps;
13
+ disabledTools: readonly string[];
14
+ }
15
+ /** Mirror of MCP `tools/list` shape. Kept minimal so the IPC layer can
16
+ * forward it as a JSON-RPC result without any SDK-specific transformations. */
17
+ export interface ToolsListResult {
18
+ tools: ReadonlyArray<{
19
+ name: string;
20
+ description: string;
21
+ inputSchema: Record<string, unknown>;
22
+ }>;
23
+ }
24
+ export declare function handleToolsList(deps: DispatchDeps): ToolsListResult;
25
+ export interface ToolCallRequest {
26
+ name: string;
27
+ arguments?: unknown;
28
+ }
29
+ export declare function handleToolCall(req: ToolCallRequest, deps: DispatchDeps): Promise<import("../tools/responses.js").ToolResponse>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure MCP request handlers — used by both the SDK Server (stdio transport)
3
+ * and the IPC bridge (TCP transport for proxies). Lives in `bridge/` because
4
+ * sharing it with the IPC server was the reason to extract these out of
5
+ * the monolithic server.ts startup.
6
+ *
7
+ * No I/O, no transport, no SDK types — just request data in, response data
8
+ * out. Both callers wrap these results in whatever envelope they need.
9
+ */
10
+ import { isToolEnabled } from '../permissions.js';
11
+ import { MAP_TOOL_DEFINITIONS, handleMapTool, isMapTool } from '../map/tools.js';
12
+ import { BROWSER_TOOL_DEFINITIONS } from '../tools/browser-definitions.js';
13
+ import { handleBrowserTool, isBrowserTool, } from '../tools/browser-dispatch.js';
14
+ import { toolError, toolResponse } from '../tools/responses.js';
15
+ export function handleToolsList(deps) {
16
+ return {
17
+ tools: [...BROWSER_TOOL_DEFINITIONS, ...MAP_TOOL_DEFINITIONS].filter((t) => isToolEnabled(t.name, deps.disabledTools)),
18
+ };
19
+ }
20
+ export async function handleToolCall(req, deps) {
21
+ const { name, arguments: args } = req;
22
+ if (!isToolEnabled(name, deps.disabledTools)) {
23
+ // Defence in depth: a client that cached the previous tools/list could
24
+ // still try to call a now-disabled tool. Refuse with a clear reason.
25
+ return toolError(`Tool "${name}" is disabled in this browser-link config. ` +
26
+ `Re-enable it via the setup UI (Permissions) or \`browser-link tools enable ${name}\`.`);
27
+ }
28
+ try {
29
+ if (isMapTool(name))
30
+ return toolResponse(handleMapTool(name, args));
31
+ if (isBrowserTool(name))
32
+ return toolResponse(await handleBrowserTool(name, args, deps.browserTools));
33
+ return toolError(`Unknown tool: ${name}`);
34
+ }
35
+ catch (err) {
36
+ return toolError(err instanceof Error ? err.message : String(err));
37
+ }
38
+ }
39
+ //# sourceMappingURL=dispatch.js.map