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.
Files changed (30) hide show
  1. package/README.md +25 -0
  2. package/apps/mcp-server/dist/db/automation-repository.js +199 -0
  3. package/apps/mcp-server/dist/db/automation-repository.js.map +1 -0
  4. package/apps/mcp-server/dist/db/connection.js +1 -5
  5. package/apps/mcp-server/dist/db/connection.js.map +1 -1
  6. package/apps/mcp-server/dist/db/events-repository.js +263 -14
  7. package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
  8. package/apps/mcp-server/dist/db/index.js +2 -0
  9. package/apps/mcp-server/dist/db/index.js.map +1 -1
  10. package/apps/mcp-server/dist/db/migrations.js +180 -0
  11. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  12. package/apps/mcp-server/dist/db/schema.js +93 -1
  13. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  14. package/apps/mcp-server/dist/main.js +54 -4
  15. package/apps/mcp-server/dist/main.js.map +1 -1
  16. package/apps/mcp-server/dist/mcp/server.js +2860 -86
  17. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  18. package/apps/mcp-server/dist/mcp-bridge.js +46 -3
  19. package/apps/mcp-server/dist/mcp-bridge.js.map +1 -1
  20. package/apps/mcp-server/dist/retention.js +67 -4
  21. package/apps/mcp-server/dist/retention.js.map +1 -1
  22. package/apps/mcp-server/dist/runtime-paths.js +33 -0
  23. package/apps/mcp-server/dist/runtime-paths.js.map +1 -0
  24. package/apps/mcp-server/dist/websocket/messages.js +30 -0
  25. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  26. package/apps/mcp-server/dist/websocket/websocket-server.js +18 -0
  27. package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
  28. package/apps/mcp-server/package.json +2 -2
  29. package/package.json +17 -6
  30. 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.6.0",
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.0",
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.0",
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.2",
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.26.0",
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",
@@ -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 launchLockPath = join(process.env.DATA_DIR ? resolve(process.env.DATA_DIR) : join(repoRoot, 'data'), '.mcp-start.lock');
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 result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8' });
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: { ...process.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: { ...process.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: { ...process.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
- : `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
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
- if (!dryRun) {
630
- acquireLaunchLock(launchLockPath);
631
- process.on('exit', () => releaseLaunchLock(launchLockPath));
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 (Number.isFinite(port)) {
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);