browser-debug-mcp-bridge 1.5.0 → 1.9.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.
- package/README.md +136 -26
- package/apps/mcp-server/dist/db/events-repository.js +296 -26
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +191 -1
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +35 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/main.js +21 -4
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +1148 -112
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/retention.js +97 -8
- package/apps/mcp-server/dist/retention.js.map +1 -1
- package/apps/mcp-server/dist/websocket/messages.js +27 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +20 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +2 -2
- package/package.json +13 -6
- package/scripts/mcp-start.cjs +178 -18
package/scripts/mcp-start.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { spawn, spawnSync } = require('node:child_process');
|
|
3
|
-
const { existsSync } = require('node:fs');
|
|
3
|
+
const { existsSync, mkdirSync, openSync, writeFileSync, closeSync, readFileSync, unlinkSync } = require('node:fs');
|
|
4
4
|
const { dirname, join, resolve } = require('node:path');
|
|
5
5
|
const { createRequire } = require('node:module');
|
|
6
6
|
const net = require('node:net');
|
|
@@ -22,6 +22,9 @@ const localRequire = createRequire(join(repoRoot, 'package.json'));
|
|
|
22
22
|
const supportsColor = Boolean(process.stderr.isTTY) && !process.env.NO_COLOR;
|
|
23
23
|
const greenBackground = '\x1b[42m\x1b[30m';
|
|
24
24
|
const ansiReset = '\x1b[0m';
|
|
25
|
+
const launchLockPath = join(process.env.DATA_DIR ? resolve(process.env.DATA_DIR) : join(repoRoot, 'data'), '.mcp-start.lock');
|
|
26
|
+
|
|
27
|
+
let launchLockHeld = false;
|
|
25
28
|
|
|
26
29
|
function resolveRuntimePath(specifier) {
|
|
27
30
|
try {
|
|
@@ -75,6 +78,117 @@ function delay(ms) {
|
|
|
75
78
|
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
function isProcessAlive(pid) {
|
|
82
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return Boolean(error && error.code === 'EPERM');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseLockPayload(raw) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readLaunchLock(lockPath) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
106
|
+
return parseLockPayload(raw);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function acquireLaunchLock(lockPath) {
|
|
113
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
114
|
+
const payload = JSON.stringify({
|
|
115
|
+
pid: process.pid,
|
|
116
|
+
createdAt: new Date().toISOString(),
|
|
117
|
+
command: process.argv.join(' '),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
const fd = openSync(lockPath, 'wx');
|
|
123
|
+
try {
|
|
124
|
+
writeFileSync(fd, payload, 'utf8');
|
|
125
|
+
} finally {
|
|
126
|
+
closeSync(fd);
|
|
127
|
+
}
|
|
128
|
+
launchLockHeld = true;
|
|
129
|
+
return;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!error || error.code !== 'EEXIST') {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existing = readLaunchLock(lockPath);
|
|
136
|
+
const lockPid = Number(existing && existing.pid);
|
|
137
|
+
if (Number.isInteger(lockPid) && lockPid > 0 && lockPid !== process.pid && isProcessAlive(lockPid)) {
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[mcp-start] MCP_STARTUP_LOCKED: another launcher instance (pid ${lockPid}) is already running.\n`,
|
|
140
|
+
);
|
|
141
|
+
process.stderr.write('[mcp-start] If startup appears stuck, run: node scripts/mcp-start.cjs --stop\n');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
unlinkSync(lockPath);
|
|
147
|
+
} catch (unlinkError) {
|
|
148
|
+
if (!unlinkError || unlinkError.code !== 'ENOENT') {
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`[mcp-start] MCP_STARTUP_LOCK_FAILED: unable to clear stale lock at ${lockPath}.\n`,
|
|
151
|
+
);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.stderr.write(`[mcp-start] MCP_STARTUP_LOCK_FAILED: unable to acquire startup lock at ${lockPath}.\n`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function releaseLaunchLock(lockPath) {
|
|
163
|
+
if (!launchLockHeld) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const existing = readLaunchLock(lockPath);
|
|
169
|
+
const lockPid = Number(existing && existing.pid);
|
|
170
|
+
if (Number.isInteger(lockPid) && lockPid > 0 && lockPid !== process.pid) {
|
|
171
|
+
launchLockHeld = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
unlinkSync(lockPath);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error && error.code !== 'ENOENT') {
|
|
177
|
+
process.stderr.write(`[mcp-start] Warning: failed to release startup lock ${lockPath}.\n`);
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
launchLockHeld = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getStartupTimeoutMs() {
|
|
185
|
+
const timeoutMs = Number(process.env.MCP_STARTUP_TIMEOUT_MS || '15000');
|
|
186
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000) {
|
|
187
|
+
return 15000;
|
|
188
|
+
}
|
|
189
|
+
return Math.floor(timeoutMs);
|
|
190
|
+
}
|
|
191
|
+
|
|
78
192
|
function fetchJson(pathname, port, timeoutMs = 1000) {
|
|
79
193
|
return new Promise((resolveJson) => {
|
|
80
194
|
const request = http.request(
|
|
@@ -261,6 +375,32 @@ async function tryRecoverStaleBridgeOnWindowsPort(port) {
|
|
|
261
375
|
return false;
|
|
262
376
|
}
|
|
263
377
|
|
|
378
|
+
async function waitForBridgeReady(port, child) {
|
|
379
|
+
const timeoutMs = getStartupTimeoutMs();
|
|
380
|
+
const deadline = Date.now() + timeoutMs;
|
|
381
|
+
|
|
382
|
+
while (Date.now() < deadline) {
|
|
383
|
+
if (child.exitCode !== null) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: `MCP bridge exited during startup with code ${child.exitCode}.`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const health = await fetchJson('/health', port, 1000);
|
|
391
|
+
if (health && typeof health === 'object' && health.status === 'ok' && health.websocket) {
|
|
392
|
+
return { ok: true };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await delay(200);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
reason: `Timed out after ${timeoutMs}ms waiting for /health on 127.0.0.1:${port}.`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
264
404
|
async function stopBridgeOnPort(port) {
|
|
265
405
|
const listenerPids = getListeningPids(port).filter((pid) => pid !== process.pid);
|
|
266
406
|
if (listenerPids.length === 0) {
|
|
@@ -336,7 +476,7 @@ if (!existsSync(packageJson)) {
|
|
|
336
476
|
process.exit(1);
|
|
337
477
|
}
|
|
338
478
|
|
|
339
|
-
function spawnRuntime(runtime) {
|
|
479
|
+
async function spawnRuntime(runtime, port) {
|
|
340
480
|
const nxTarget = standalone ? 'mcp-server:serve' : 'mcp-server:serve-mcp';
|
|
341
481
|
const entryScript =
|
|
342
482
|
runtime === 'dist'
|
|
@@ -359,17 +499,6 @@ function spawnRuntime(runtime) {
|
|
|
359
499
|
process.exit(0);
|
|
360
500
|
}
|
|
361
501
|
|
|
362
|
-
const startedMessage = standalone
|
|
363
|
-
? `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: standalone). Keep this terminal open.`
|
|
364
|
-
: `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
|
|
365
|
-
process.stderr.write(`${supportsColor ? `${greenBackground}${startedMessage}${ansiReset}` : startedMessage}\n`);
|
|
366
|
-
if (!standalone && process.stdin.isTTY) {
|
|
367
|
-
process.stderr.write(
|
|
368
|
-
'[mcp-start] Running from interactive terminal without MCP host. ' +
|
|
369
|
-
'Use --standalone for manual keep-alive testing.\n',
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
502
|
const child =
|
|
374
503
|
runtime === 'nx'
|
|
375
504
|
? spawn(
|
|
@@ -397,6 +526,8 @@ function spawnRuntime(runtime) {
|
|
|
397
526
|
},
|
|
398
527
|
);
|
|
399
528
|
|
|
529
|
+
let startupFinished = false;
|
|
530
|
+
|
|
400
531
|
const forwardSignalToChild = (signal) => {
|
|
401
532
|
if (child.exitCode !== null || child.killed) {
|
|
402
533
|
return;
|
|
@@ -441,6 +572,11 @@ function spawnRuntime(runtime) {
|
|
|
441
572
|
process.kill(process.pid, signal);
|
|
442
573
|
return;
|
|
443
574
|
}
|
|
575
|
+
if (!startupFinished) {
|
|
576
|
+
process.stderr.write(
|
|
577
|
+
`[mcp-start] MCP_STARTUP_FAILED: MCP bridge exited before readiness check completed (code ${code ?? 0}).\n`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
444
580
|
if (!standalone && process.stdin.isTTY && (code ?? 0) === 0) {
|
|
445
581
|
process.stderr.write(
|
|
446
582
|
'[mcp-start] MCP stdio process exited (no MCP host attached). ' +
|
|
@@ -457,6 +593,25 @@ function spawnRuntime(runtime) {
|
|
|
457
593
|
);
|
|
458
594
|
process.exit(1);
|
|
459
595
|
});
|
|
596
|
+
|
|
597
|
+
const readiness = await waitForBridgeReady(port, child);
|
|
598
|
+
if (!readiness.ok) {
|
|
599
|
+
process.stderr.write(`[mcp-start] MCP_STARTUP_FAILED: ${readiness.reason}\n`);
|
|
600
|
+
forwardSignalToChild('SIGTERM');
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
startupFinished = true;
|
|
605
|
+
const startedMessage = standalone
|
|
606
|
+
? `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: standalone). Keep this terminal open.`
|
|
607
|
+
: `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
|
|
608
|
+
process.stderr.write(`${supportsColor ? `${greenBackground}${startedMessage}${ansiReset}` : startedMessage}\n`);
|
|
609
|
+
if (!standalone && process.stdin.isTTY) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
'[mcp-start] Running from interactive terminal without MCP host. ' +
|
|
612
|
+
'Use --standalone for manual keep-alive testing.\n',
|
|
613
|
+
);
|
|
614
|
+
}
|
|
460
615
|
}
|
|
461
616
|
|
|
462
617
|
async function main() {
|
|
@@ -471,6 +626,11 @@ async function main() {
|
|
|
471
626
|
return;
|
|
472
627
|
}
|
|
473
628
|
|
|
629
|
+
if (!dryRun) {
|
|
630
|
+
acquireLaunchLock(launchLockPath);
|
|
631
|
+
process.on('exit', () => releaseLaunchLock(launchLockPath));
|
|
632
|
+
}
|
|
633
|
+
|
|
474
634
|
if (Number.isFinite(port)) {
|
|
475
635
|
let inUse = await isPortInUse(port);
|
|
476
636
|
if (inUse) {
|
|
@@ -506,7 +666,7 @@ async function main() {
|
|
|
506
666
|
);
|
|
507
667
|
process.exit(1);
|
|
508
668
|
}
|
|
509
|
-
spawnRuntime('dist');
|
|
669
|
+
await spawnRuntime('dist', port);
|
|
510
670
|
return;
|
|
511
671
|
}
|
|
512
672
|
|
|
@@ -515,17 +675,17 @@ async function main() {
|
|
|
515
675
|
process.stderr.write('[mcp-start] Missing tsx runtime. Run npm install/pnpm install first.\n');
|
|
516
676
|
process.exit(1);
|
|
517
677
|
}
|
|
518
|
-
spawnRuntime('tsx');
|
|
678
|
+
await spawnRuntime('tsx', port);
|
|
519
679
|
return;
|
|
520
680
|
}
|
|
521
681
|
|
|
522
682
|
if (existsSync(mcpBridgeDistEntry) && existsSync(mainServerDistEntry)) {
|
|
523
|
-
spawnRuntime('dist');
|
|
683
|
+
await spawnRuntime('dist', port);
|
|
524
684
|
return;
|
|
525
685
|
}
|
|
526
686
|
|
|
527
687
|
if (existsSync(nxBin)) {
|
|
528
|
-
spawnRuntime('nx');
|
|
688
|
+
await spawnRuntime('nx', port);
|
|
529
689
|
return;
|
|
530
690
|
}
|
|
531
691
|
|
|
@@ -538,7 +698,7 @@ async function main() {
|
|
|
538
698
|
}
|
|
539
699
|
|
|
540
700
|
process.stderr.write('[mcp-start] nx runtime not found, using tsx fallback runtime.\n');
|
|
541
|
-
spawnRuntime('tsx');
|
|
701
|
+
await spawnRuntime('tsx', port);
|
|
542
702
|
}
|
|
543
703
|
|
|
544
704
|
main().catch((error) => {
|