browser-debug-mcp-bridge 1.6.0 → 1.10.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 +25 -0
- package/apps/mcp-server/dist/db/automation-repository.js +199 -0
- package/apps/mcp-server/dist/db/automation-repository.js.map +1 -0
- package/apps/mcp-server/dist/db/connection.js +1 -5
- package/apps/mcp-server/dist/db/connection.js.map +1 -1
- package/apps/mcp-server/dist/db/events-repository.js +263 -14
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/index.js +2 -0
- package/apps/mcp-server/dist/db/index.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +180 -0
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +93 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/main.js +54 -4
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +2860 -86
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/mcp-bridge.js +46 -3
- package/apps/mcp-server/dist/mcp-bridge.js.map +1 -1
- package/apps/mcp-server/dist/retention.js +67 -4
- package/apps/mcp-server/dist/retention.js.map +1 -1
- package/apps/mcp-server/dist/runtime-paths.js +33 -0
- package/apps/mcp-server/dist/runtime-paths.js.map +1 -0
- package/apps/mcp-server/dist/websocket/messages.js +30 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +18 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +2 -2
- package/package.json +17 -6
- package/scripts/mcp-start.cjs +201 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-debug-mcp-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Chrome Extension + Node.js MCP server for browser debugging",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,11 +19,17 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"typecheck": "nx run-many -t lint --projects mcp-server,chrome-extension,shared,redaction,selectors,mcp-contracts",
|
|
21
21
|
"test": "nx run-many -t test",
|
|
22
|
+
"test:non-e2e": "nx run-many -t test --exclude=e2e-playwright",
|
|
22
23
|
"lint": "nx run-many -t lint",
|
|
23
24
|
"build": "nx run-many -t build",
|
|
25
|
+
"verify:ci": "pnpm typecheck && pnpm lint && pnpm test:non-e2e && pnpm build && pnpm docs:ci && pnpm mcp:check-stdio-guard",
|
|
24
26
|
"verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm docs:ci && pnpm mcp:check-stdio-guard",
|
|
25
27
|
"serve": "nx serve mcp-server",
|
|
26
28
|
"mcp:start": "node scripts/mcp-start.cjs",
|
|
29
|
+
"mcp:diagnose": "node tools/mcp/diagnose-bridge.mjs",
|
|
30
|
+
"mcp:doctor": "node tools/mcp/diagnose-bridge.mjs",
|
|
31
|
+
"mcp:doctor:json": "node tools/mcp/diagnose-bridge.mjs --json",
|
|
32
|
+
"mcp:smoke": "node tools/mcp/diagnose-bridge.mjs --smoke",
|
|
27
33
|
"build:mcp-runtime": "pnpm -C apps/mcp-server build",
|
|
28
34
|
"prepack": "pnpm run build:mcp-runtime",
|
|
29
35
|
"docs:dev": "nx serve docs",
|
|
@@ -43,7 +49,11 @@
|
|
|
43
49
|
"release:tag:yes": "node tools/release/create-tag.mjs --yes",
|
|
44
50
|
"mcp:print-config": "node tools/mcp/print-client-config.mjs",
|
|
45
51
|
"mcp:check-stdio-guard": "node tools/mcp/check-stdio-guard.mjs",
|
|
46
|
-
"hooks:install": "git config core.hooksPath .githooks && git config commit.template .gitmessage.txt"
|
|
52
|
+
"hooks:install": "git config core.hooksPath .githooks && git config commit.template .gitmessage.txt",
|
|
53
|
+
"test:e2e": "nx test e2e-playwright",
|
|
54
|
+
"test:e2e:head": "node tools/e2e/run-headed-e2e.mjs",
|
|
55
|
+
"test:e2e:smoke": "nx run e2e-playwright:smoke",
|
|
56
|
+
"test:e2e:full": "nx run e2e-playwright:full"
|
|
47
57
|
},
|
|
48
58
|
"keywords": [
|
|
49
59
|
"mcp",
|
|
@@ -59,17 +69,18 @@
|
|
|
59
69
|
"devDependencies": {
|
|
60
70
|
"@docusaurus/core": "^3.9.2",
|
|
61
71
|
"@docusaurus/preset-classic": "^3.9.2",
|
|
62
|
-
"@easyops-cn/docusaurus-search-local": "^0.55.
|
|
72
|
+
"@easyops-cn/docusaurus-search-local": "^0.55.1",
|
|
63
73
|
"@mdx-js/react": "^3.1.1",
|
|
64
74
|
"@nx/vite": "^22.5.2",
|
|
75
|
+
"@playwright/test": "^1.58.2",
|
|
65
76
|
"@types/chrome": "^0.1.37",
|
|
66
|
-
"@types/node": "^25.3.
|
|
77
|
+
"@types/node": "^25.3.3",
|
|
67
78
|
"@types/react": "^19.2.14",
|
|
68
79
|
"@types/react-dom": "^19.2.3",
|
|
69
80
|
"@vitest/coverage-v8": "^4.0.18",
|
|
70
81
|
"jsdom": "^28.1.0",
|
|
71
82
|
"markdownlint-cli2": "^0.21.0",
|
|
72
|
-
"nx": "^22.5.
|
|
83
|
+
"nx": "^22.5.3",
|
|
73
84
|
"react": "^19.2.4",
|
|
74
85
|
"react-dom": "^19.2.4",
|
|
75
86
|
"typescript": "^5.9.3",
|
|
@@ -77,7 +88,7 @@
|
|
|
77
88
|
"vitest": "^4.0.18"
|
|
78
89
|
},
|
|
79
90
|
"dependencies": {
|
|
80
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
91
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
81
92
|
"better-sqlite3": "^12.6.2",
|
|
82
93
|
"fastify": "^5.7.4",
|
|
83
94
|
"jszip": "^3.10.1",
|
package/scripts/mcp-start.cjs
CHANGED
|
@@ -5,8 +5,10 @@ const { dirname, join, resolve } = require('node:path');
|
|
|
5
5
|
const { createRequire } = require('node:module');
|
|
6
6
|
const net = require('node:net');
|
|
7
7
|
const http = require('node:http');
|
|
8
|
+
const { homedir } = require('node:os');
|
|
8
9
|
|
|
9
10
|
const repoRoot = resolve(__dirname, '..');
|
|
11
|
+
const runtimeDirName = 'browser-debug-mcp-bridge';
|
|
10
12
|
const packageJson = join(repoRoot, 'package.json');
|
|
11
13
|
const mcpBridgeDistEntry = join(repoRoot, 'apps', 'mcp-server', 'dist', 'mcp-bridge.js');
|
|
12
14
|
const mainServerDistEntry = join(repoRoot, 'apps', 'mcp-server', 'dist', 'main.js');
|
|
@@ -22,10 +24,45 @@ const localRequire = createRequire(join(repoRoot, 'package.json'));
|
|
|
22
24
|
const supportsColor = Boolean(process.stderr.isTTY) && !process.env.NO_COLOR;
|
|
23
25
|
const greenBackground = '\x1b[42m\x1b[30m';
|
|
24
26
|
const ansiReset = '\x1b[0m';
|
|
25
|
-
const
|
|
27
|
+
const resolvedDataDir = resolveRuntimeDataDir();
|
|
28
|
+
const launchLockPath = join(resolvedDataDir, '.mcp-start.lock');
|
|
26
29
|
|
|
27
30
|
let launchLockHeld = false;
|
|
28
31
|
|
|
32
|
+
function resolveRuntimeDataDir() {
|
|
33
|
+
const explicitDataDir = process.env.DATA_DIR && process.env.DATA_DIR.trim();
|
|
34
|
+
if (explicitDataDir) {
|
|
35
|
+
return resolve(explicitDataDir);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const home = process.env.HOME || homedir();
|
|
39
|
+
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
const appDataRoot = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
42
|
+
if (appDataRoot) {
|
|
43
|
+
return resolve(appDataRoot, runtimeDirName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.platform === 'darwin' && home) {
|
|
48
|
+
return resolve(home, 'Library', 'Application Support', runtimeDirName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.env.XDG_STATE_HOME) {
|
|
52
|
+
return resolve(process.env.XDG_STATE_HOME, runtimeDirName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.env.XDG_DATA_HOME) {
|
|
56
|
+
return resolve(process.env.XDG_DATA_HOME, runtimeDirName);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (home) {
|
|
60
|
+
return resolve(home, '.local', 'share', runtimeDirName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return resolve(process.cwd(), '.browser-debug-mcp-bridge');
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
function resolveRuntimePath(specifier) {
|
|
30
67
|
try {
|
|
31
68
|
return localRequire.resolve(specifier);
|
|
@@ -181,6 +218,115 @@ function releaseLaunchLock(lockPath) {
|
|
|
181
218
|
}
|
|
182
219
|
}
|
|
183
220
|
|
|
221
|
+
function clearLaunchLockForPid(lockPath, pid) {
|
|
222
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const existing = readLaunchLock(lockPath);
|
|
228
|
+
const lockPid = Number(existing && existing.pid);
|
|
229
|
+
if (lockPid !== pid) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
unlinkSync(lockPath);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error && error.code !== 'ENOENT') {
|
|
235
|
+
process.stderr.write(`[mcp-start] Warning: failed to clear startup lock ${lockPath}.\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isStandaloneLauncherCommand(command) {
|
|
241
|
+
const normalized = String(command || '').toLowerCase();
|
|
242
|
+
return (
|
|
243
|
+
(normalized.includes('scripts\\mcp-start.cjs') || normalized.includes('scripts/mcp-start.cjs'))
|
|
244
|
+
&& normalized.includes('--standalone')
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function tryRecoverLockedStandaloneForMcpStdio(lockPath) {
|
|
249
|
+
if (standalone || dryRun || stopRequested) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const existing = readLaunchLock(lockPath);
|
|
254
|
+
const lockPid = Number(existing && existing.pid);
|
|
255
|
+
if (!Number.isInteger(lockPid) || lockPid <= 0 || lockPid === process.pid) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!isProcessAlive(lockPid) || !isStandaloneLauncherCommand(existing && existing.command)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
process.stderr.write(
|
|
264
|
+
`[mcp-start] Replacing running standalone launcher (pid ${lockPid}) so MCP stdio can own the bridge runtime.\n`,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (!terminateProcess(lockPid)) {
|
|
268
|
+
process.stderr.write(`[mcp-start] Failed to terminate standalone launcher ${lockPid}.\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
273
|
+
await delay(200);
|
|
274
|
+
if (!isProcessAlive(lockPid)) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isProcessAlive(lockPid)) {
|
|
280
|
+
process.stderr.write(
|
|
281
|
+
`[mcp-start] Standalone launcher ${lockPid} did not exit after termination request.\n`,
|
|
282
|
+
);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clearLaunchLockForPid(lockPath, lockPid);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function tryReplaceExistingBridgeForMcpStdio(port) {
|
|
291
|
+
if (standalone || dryRun || stopRequested) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const endpointLooksLikeBridge = await isBridgeHttpEndpoint(port);
|
|
296
|
+
if (!endpointLooksLikeBridge) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const listenerPids = getListeningPids(port).filter((pid) => pid !== process.pid);
|
|
301
|
+
if (listenerPids.length === 0) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
process.stderr.write(
|
|
306
|
+
`[mcp-start] Replacing existing Browser Debug MCP Bridge listener(s) on port ${port} so MCP stdio can own the runtime.\n`,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
for (const pid of listenerPids) {
|
|
310
|
+
if (!terminateProcess(pid)) {
|
|
311
|
+
process.stderr.write(`[mcp-start] Failed to terminate bridge listener ${pid} on port ${port}.\n`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
317
|
+
await delay(200);
|
|
318
|
+
const inUse = await isPortInUse(port);
|
|
319
|
+
if (!inUse) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
process.stderr.write(
|
|
325
|
+
`[mcp-start] Existing bridge listeners stopped, but port ${port} is still in use.\n`,
|
|
326
|
+
);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
184
330
|
function getStartupTimeoutMs() {
|
|
185
331
|
const timeoutMs = Number(process.env.MCP_STARTUP_TIMEOUT_MS || '15000');
|
|
186
332
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000) {
|
|
@@ -264,7 +410,7 @@ function getWindowsProcessCommandLine(pid) {
|
|
|
264
410
|
const result = spawnSync(
|
|
265
411
|
'powershell.exe',
|
|
266
412
|
['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`],
|
|
267
|
-
{ encoding: 'utf8' },
|
|
413
|
+
{ encoding: 'utf8', timeout: 2000 },
|
|
268
414
|
);
|
|
269
415
|
|
|
270
416
|
if (result.status !== 0) {
|
|
@@ -285,7 +431,16 @@ function isLikelyBridgeCommandLine(commandLine) {
|
|
|
285
431
|
}
|
|
286
432
|
|
|
287
433
|
function killWindowsProcess(pid) {
|
|
288
|
-
const
|
|
434
|
+
const stopResult = spawnSync(
|
|
435
|
+
'powershell.exe',
|
|
436
|
+
['-NoProfile', '-Command', `Stop-Process -Id ${pid} -Force`],
|
|
437
|
+
{ encoding: 'utf8', timeout: 5000 },
|
|
438
|
+
);
|
|
439
|
+
if (stopResult.status === 0) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8', timeout: 5000 });
|
|
289
444
|
return result.status === 0;
|
|
290
445
|
}
|
|
291
446
|
|
|
@@ -477,6 +632,7 @@ if (!existsSync(packageJson)) {
|
|
|
477
632
|
}
|
|
478
633
|
|
|
479
634
|
async function spawnRuntime(runtime, port) {
|
|
635
|
+
const attachExistingBridge = !standalone && process.env.MCP_ATTACH_EXISTING_BRIDGE === '1';
|
|
480
636
|
const nxTarget = standalone ? 'mcp-server:serve' : 'mcp-server:serve-mcp';
|
|
481
637
|
const entryScript =
|
|
482
638
|
runtime === 'dist'
|
|
@@ -506,14 +662,24 @@ async function spawnRuntime(runtime, port) {
|
|
|
506
662
|
nxBin.endsWith('.cmd') ? ['run', nxTarget] : [nxBin, 'run', nxTarget],
|
|
507
663
|
{
|
|
508
664
|
cwd: repoRoot,
|
|
509
|
-
env: {
|
|
665
|
+
env: {
|
|
666
|
+
...process.env,
|
|
667
|
+
DATA_DIR: resolvedDataDir,
|
|
668
|
+
MCP_ATTACH_EXISTING_BRIDGE: attachExistingBridge ? '1' : '',
|
|
669
|
+
MCP_ATTACH_HTTP_BASE_URL: `http://127.0.0.1:${port}`,
|
|
670
|
+
},
|
|
510
671
|
stdio: 'inherit',
|
|
511
672
|
},
|
|
512
673
|
)
|
|
513
674
|
: runtime === 'dist'
|
|
514
675
|
? spawn(process.execPath, [entryScript], {
|
|
515
676
|
cwd: repoRoot,
|
|
516
|
-
env: {
|
|
677
|
+
env: {
|
|
678
|
+
...process.env,
|
|
679
|
+
DATA_DIR: resolvedDataDir,
|
|
680
|
+
MCP_ATTACH_EXISTING_BRIDGE: attachExistingBridge ? '1' : '',
|
|
681
|
+
MCP_ATTACH_HTTP_BASE_URL: `http://127.0.0.1:${port}`,
|
|
682
|
+
},
|
|
517
683
|
stdio: 'inherit',
|
|
518
684
|
})
|
|
519
685
|
: spawn(
|
|
@@ -521,7 +687,12 @@ async function spawnRuntime(runtime, port) {
|
|
|
521
687
|
tsxCli.endsWith('.cmd') ? [entryScript] : [tsxCli, entryScript],
|
|
522
688
|
{
|
|
523
689
|
cwd: repoRoot,
|
|
524
|
-
env: {
|
|
690
|
+
env: {
|
|
691
|
+
...process.env,
|
|
692
|
+
DATA_DIR: resolvedDataDir,
|
|
693
|
+
MCP_ATTACH_EXISTING_BRIDGE: attachExistingBridge ? '1' : '',
|
|
694
|
+
MCP_ATTACH_HTTP_BASE_URL: `http://127.0.0.1:${port}`,
|
|
695
|
+
},
|
|
525
696
|
stdio: 'inherit',
|
|
526
697
|
},
|
|
527
698
|
);
|
|
@@ -604,7 +775,9 @@ async function spawnRuntime(runtime, port) {
|
|
|
604
775
|
startupFinished = true;
|
|
605
776
|
const startedMessage = standalone
|
|
606
777
|
? `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: standalone). Keep this terminal open.`
|
|
607
|
-
:
|
|
778
|
+
: attachExistingBridge
|
|
779
|
+
? `[mcp-start] Attached MCP stdio to existing Browser Debug MCP Bridge (runtime: ${runtime}, mode: attach).`
|
|
780
|
+
: `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
|
|
608
781
|
process.stderr.write(`${supportsColor ? `${greenBackground}${startedMessage}${ansiReset}` : startedMessage}\n`);
|
|
609
782
|
if (!standalone && process.stdin.isTTY) {
|
|
610
783
|
process.stderr.write(
|
|
@@ -612,6 +785,13 @@ async function spawnRuntime(runtime, port) {
|
|
|
612
785
|
'Use --standalone for manual keep-alive testing.\n',
|
|
613
786
|
);
|
|
614
787
|
}
|
|
788
|
+
|
|
789
|
+
if (standalone) {
|
|
790
|
+
await new Promise((resolve) => {
|
|
791
|
+
child.once('exit', () => resolve());
|
|
792
|
+
child.once('close', () => resolve());
|
|
793
|
+
});
|
|
794
|
+
}
|
|
615
795
|
}
|
|
616
796
|
|
|
617
797
|
async function main() {
|
|
@@ -626,12 +806,22 @@ async function main() {
|
|
|
626
806
|
return;
|
|
627
807
|
}
|
|
628
808
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
809
|
+
let attachExistingBridge = false;
|
|
810
|
+
if (!standalone && !dryRun) {
|
|
811
|
+
attachExistingBridge = await isBridgeHttpEndpoint(port);
|
|
812
|
+
if (attachExistingBridge) {
|
|
813
|
+
process.env.MCP_ATTACH_EXISTING_BRIDGE = '1';
|
|
814
|
+
process.stderr.write(
|
|
815
|
+
`[mcp-start] Found existing Browser Debug MCP Bridge on port ${port}; attaching MCP stdio instead of replacing it.\n`,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
632
818
|
}
|
|
633
819
|
|
|
634
|
-
if (
|
|
820
|
+
if (!dryRun && !attachExistingBridge) {
|
|
821
|
+
await tryRecoverLockedStandaloneForMcpStdio(launchLockPath);
|
|
822
|
+
await tryReplaceExistingBridgeForMcpStdio(port);
|
|
823
|
+
acquireLaunchLock(launchLockPath);
|
|
824
|
+
process.on('exit', () => releaseLaunchLock(launchLockPath));
|
|
635
825
|
let inUse = await isPortInUse(port);
|
|
636
826
|
if (inUse) {
|
|
637
827
|
const recovered = await tryRecoverStaleBridgeOnWindowsPort(port);
|