browser-debug-mcp-bridge 1.4.0 → 1.5.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 CHANGED
@@ -1,94 +1,204 @@
1
1
  # Browser Debug MCP Bridge
2
2
 
3
- Chrome Extension + local Node.js MCP server that captures browser debugging telemetry from a real user session and exposes it through MCP tools.
3
+ Chrome Extension + local Node.js MCP runtime for real-browser debugging.
4
4
 
5
- ## Why this project exists
5
+ It captures telemetry from an actual browser session (console, network, navigation, UI events), stores it locally, and exposes debugging tools through MCP to your AI client.
6
6
 
7
- - Debug with real browser context (logged-in sessions, feature flags, extensions)
8
- - Store lightweight telemetry continuously, request heavy DOM data on demand
9
- - Keep privacy-first defaults with safe mode, allowlists, and redaction
7
+ ## What You Can Do
10
8
 
11
- ## Prerequisites
9
+ - Inspect real sessions instead of synthetic test runs
10
+ - Query recent errors, failed requests, and event timelines
11
+ - Run targeted live capture (DOM subtree/document, styles, layout)
12
+ - Correlate user actions with network/runtime failures
13
+ - Keep privacy controls enabled (safe mode, allowlist, redaction)
12
14
 
13
- - Node.js 20+
14
- - pnpm 9+
15
- - Chrome (for the extension)
15
+ ## How It Works
16
16
 
17
- ## Quick start
17
+ 1. Chrome extension captures session telemetry.
18
+ 2. Local server ingests via HTTP/WebSocket on `127.0.0.1:8065`.
19
+ 3. Data is persisted in local SQLite.
20
+ 4. MCP stdio server exposes tools to your AI client.
21
+
22
+ ## Requirements
23
+
24
+ - Node.js `>=20`
25
+ - pnpm `>=9` (for local repo mode)
26
+ - Chrome (Developer Mode to load unpacked extension)
27
+
28
+ ## Setup Modes
29
+
30
+ ### Recommended: Full Local Setup (MCP + Extension)
31
+
32
+ Use this when you want the full product (including extension).
18
33
 
19
34
  ```bash
35
+ git clone https://github.com/RobertoM80/browser-debug-mcp-bridge.git
36
+ cd browser-debug-mcp-bridge
20
37
  pnpm install
21
- pnpm nx serve mcp-server
22
- pnpm nx build chrome-extension --watch
38
+ pnpm nx build mcp-server
39
+ pnpm nx build chrome-extension
23
40
  ```
24
41
 
25
- Run unified ingest + MCP stdio runtime (for external MCP clients):
42
+ Load extension:
43
+
44
+ 1. Open `chrome://extensions`
45
+ 2. Enable Developer mode
46
+ 3. Click **Load unpacked**
47
+ 4. Select `dist/apps/chrome-extension`
48
+
49
+ Start MCP runtime:
26
50
 
27
51
  ```bash
28
- pnpm install
29
52
  node scripts/mcp-start.cjs
30
53
  ```
31
54
 
32
- Quick npm MCP launch (marketplace-style, after publish):
55
+ ### Quick Runtime (MCP server launcher only)
56
+
57
+ If you already have extension/runtime assets aligned, you can launch from npm:
33
58
 
34
59
  ```bash
35
60
  npx -y browser-debug-mcp-bridge
36
61
  ```
37
62
 
38
- Note: npm mode starts the MCP server runtime. The Chrome extension still needs to be built/loaded separately (see "Load the extension").
39
-
40
- GitHub fallback launch (if npm package is not available yet):
63
+ GitHub fallback (if npm registry package is unavailable):
41
64
 
42
65
  ```bash
43
66
  npx -y --package=github:RobertoM80/browser-debug-mcp-bridge browser-debug-mcp-bridge
44
67
  ```
45
68
 
46
- Optional one-step setup scripts:
69
+ Important:
70
+
71
+ - This only starts the runtime.
72
+ - You still need a compatible extension connected to `127.0.0.1:8065`.
73
+
74
+ ## MCP Client Configuration
75
+
76
+ Generate ready-to-paste snippets:
47
77
 
48
78
  ```bash
49
- # Windows (PowerShell)
50
- ./install.ps1
79
+ pnpm mcp:print-config
80
+ ```
51
81
 
52
- # macOS/Linux
53
- bash ./install.sh
82
+ ### OpenAI (Codex CLI / Codex in VS Code)
83
+
84
+ Edit `~/.codex/config.toml` (Windows: `C:\Users\<you>\.codex\config.toml`) and add:
85
+
86
+ ```toml
87
+ [mcp_servers.browser_debug]
88
+ command = "node"
89
+ args = ["C:\\ABSOLUTE\\PATH\\TO\\browser-debug-mcp-bridge\\scripts\\mcp-start.cjs"]
90
+ ```
91
+
92
+ npm quick mode:
93
+
94
+ ```toml
95
+ [mcp_servers.browser_debug]
96
+ command = "npx"
97
+ args = ["-y", "browser-debug-mcp-bridge"]
98
+ ```
99
+
100
+ ### OpenCode
101
+
102
+ Use JSON MCP config:
103
+
104
+ ```json
105
+ {
106
+ "mcpServers": {
107
+ "browser-debug": {
108
+ "command": "node",
109
+ "args": [
110
+ "C:\\ABSOLUTE\\PATH\\TO\\browser-debug-mcp-bridge\\scripts\\mcp-start.cjs"
111
+ ]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### VS Code (any MCP host expecting command/args)
118
+
119
+ Use the same values:
120
+
121
+ - `command`: `node`
122
+ - `args`: `[
123
+ "<ABSOLUTE_PATH>/scripts/mcp-start.cjs"
124
+ ]`
125
+
126
+ If your VS Code MCP host uses JSON, reuse the OpenCode JSON block above.
127
+
128
+ ## First End-to-End Check
129
+
130
+ - Start MCP host/client (so it launches this server).
131
+ - Open extension popup, allowlist domain, start a session.
132
+ - Ask your AI client to run:
133
+
134
+ ```json
135
+ { "name": "list_sessions", "arguments": { "sinceMinutes": 60 } }
136
+ ```
137
+
138
+ - Pick a session where `liveConnection.connected` is `true`.
139
+ - Run query tools first (`get_session_summary`, `get_recent_events`, `get_network_failures`).
140
+ - Use live tools (`get_dom_document`, `capture_ui_snapshot`) only on connected sessions.
141
+
142
+ ## Port and Startup Behavior
143
+
144
+ Default port is `8065`.
145
+
146
+ - On Windows, launcher tries automatic stale bridge recovery first.
147
+ - If port is still occupied, startup fails with `MCP_STARTUP_PORT_IN_USE`.
148
+ - In that case, free/reserve port `8065` for this bridge and restart.
149
+ - In `mcp-stdio` mode, bridge lifecycle is tied to the host and should stop when host transport closes.
150
+ - If a stale process still remains, stop it explicitly with `node scripts/mcp-start.cjs --stop`.
151
+
152
+ Useful Windows command:
153
+
154
+ ```powershell
155
+ netstat -ano | findstr :8065
54
156
  ```
55
157
 
56
- Useful workspace commands:
158
+ Stop command:
57
159
 
58
160
  ```bash
59
- pnpm typecheck
60
- pnpm test
61
- pnpm nx run-many -t lint
62
- pnpm nx run-many -t build
161
+ node scripts/mcp-start.cjs --stop
63
162
  ```
64
163
 
65
- Enable local pre-commit checks (typecheck + lint + test before each commit):
164
+ ## Common Failure Signals
165
+
166
+ - `LIVE_SESSION_DISCONNECTED`: session exists in DB but no active extension transport. Fix: restart/reconnect extension session, then use a `liveConnection.connected = true` session id.
167
+ - `MCP_STARTUP_PORT_IN_USE`: required MCP port is blocked. Fix: stop the process using that port and restart bridge.
168
+
169
+ ## Useful Commands
66
170
 
67
171
  ```bash
68
- pnpm hooks:install
172
+ pnpm typecheck
173
+ pnpm lint
174
+ pnpm test
175
+ pnpm build
176
+ pnpm docs:ci
177
+ pnpm verify
178
+ node scripts/mcp-start.cjs --stop
69
179
  ```
70
180
 
71
- ## Load the extension
181
+ Optional one-shot local setup:
72
182
 
73
- 1. Build the extension: `pnpm nx build chrome-extension`
74
- 2. Open Chrome -> `chrome://extensions`
75
- 3. Enable Developer mode
76
- 4. Click **Load unpacked**
77
- 5. Select `dist/apps/chrome-extension`
183
+ ```powershell
184
+ # Windows
185
+ ./install.ps1
186
+ ```
187
+
188
+ ```bash
189
+ # macOS/Linux
190
+ bash ./install.sh
191
+ ```
78
192
 
79
- ## Main docs
193
+ ## Tooling Docs
80
194
 
81
- - Project spec: `PROJECT_INFOS.md`
82
- - Full beginner setup guide: `HOW_TO_USE_BROWSER_DEBUG_MCP_BRIDGE.md`
83
- - MCP tools reference: `docs/MCP_TOOLS.md`
84
- - MCP client setup (Codex/Claude/Cursor/Windsurf): `docs/MCP_CLIENT_SETUP.md`
85
- - GitHub Actions explained: `docs/GITHUB_ACTIONS.md`
86
- - Security and privacy controls: `SECURITY.md`
87
- - Troubleshooting guide: `docs/TROUBLESHOOTING.md`
88
- - Architecture overview: `docs/ARCHITECTURE.md`
89
- - Architecture decisions: `docs/ARCHITECTURE_DECISIONS.md`
195
+ - [MCP tools reference](https://github.com/RobertoM80/browser-debug-mcp-bridge/blob/main/docs/MCP_TOOLS.md)
196
+ - [MCP client setup](https://github.com/RobertoM80/browser-debug-mcp-bridge/blob/main/docs/MCP_CLIENT_SETUP.md)
197
+ - [Troubleshooting](https://github.com/RobertoM80/browser-debug-mcp-bridge/blob/main/docs/TROUBLESHOOTING.md)
198
+ - [Architecture](https://github.com/RobertoM80/browser-debug-mcp-bridge/blob/main/docs/ARCHITECTURE.md)
199
+ - [Security and privacy](https://github.com/RobertoM80/browser-debug-mcp-bridge/blob/main/SECURITY.md)
90
200
 
91
- ## Repository layout
201
+ ## Repository Layout
92
202
 
93
203
  ```text
94
204
  apps/
@@ -98,6 +208,6 @@ apps/
98
208
  libs/
99
209
  shared/ Shared schemas/types/utils
100
210
  redaction/ Privacy redaction engine
101
- selectors/ Robust selector generation
102
- mcp-contracts/ MCP tool contracts and schemas
211
+ selectors/ Selector generation
212
+ mcp-contracts/ MCP tool contracts and schemas
103
213
  ```
@@ -225,6 +225,7 @@ const DEFAULT_EVENT_LIMIT = 50;
225
225
  const MAX_LIMIT = 200;
226
226
  const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
227
227
  const MAX_SNAPSHOT_ASSET_CHUNK_BYTES = 256 * 1024;
228
+ const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
228
229
  const NETWORK_DOMAIN_GROUP_SQL = `
229
230
  CASE
230
231
  WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
@@ -236,6 +237,16 @@ const NETWORK_DOMAIN_GROUP_SQL = `
236
237
  ELSE replace(replace(url, 'https://', ''), 'http://', '')
237
238
  END
238
239
  `;
240
+ class LiveSessionDisconnectedError extends Error {
241
+ code = LIVE_SESSION_DISCONNECTED_CODE;
242
+ constructor(sessionId, reason) {
243
+ const normalizedReason = typeof reason === 'string' && reason.trim().length > 0
244
+ ? reason.trim()
245
+ : 'Extension connection is stale or unavailable';
246
+ super(`${LIVE_SESSION_DISCONNECTED_CODE}: Session ${sessionId} is not connected to a live extension target. ${normalizedReason}. Start a fresh session in the extension and retry with a connected sessionId from list_sessions.`);
247
+ this.name = 'LiveSessionDisconnectedError';
248
+ }
249
+ }
239
250
  function resolveLimit(value, fallback) {
240
251
  if (typeof value !== 'number' || !Number.isFinite(value)) {
241
252
  return fallback;
@@ -517,13 +528,42 @@ function asStringArray(value, maxItems) {
517
528
  .filter((entry) => typeof entry === 'string' && entry.length > 0)
518
529
  .slice(0, maxItems);
519
530
  }
520
- function ensureCaptureSuccess(result) {
531
+ function isLiveSessionDisconnectedMessage(message) {
532
+ const normalized = message.toLowerCase();
533
+ return normalized.includes('no active extension connection')
534
+ || normalized.includes('receiving end does not exist')
535
+ || normalized.includes('could not establish connection')
536
+ || normalized.includes('connection closed before capture completed')
537
+ || normalized.includes('websocket manager closed')
538
+ || normalized.includes('extension target is unavailable')
539
+ || normalized.includes('target tab for this session is unavailable');
540
+ }
541
+ function normalizeCaptureError(sessionId, error) {
542
+ const fallback = error instanceof Error ? error : new Error(String(error));
543
+ const message = fallback.message ?? '';
544
+ if (isLiveSessionDisconnectedMessage(message)) {
545
+ return new LiveSessionDisconnectedError(sessionId, message);
546
+ }
547
+ return fallback;
548
+ }
549
+ function isLiveSessionDisconnectedError(error) {
550
+ return error instanceof LiveSessionDisconnectedError;
551
+ }
552
+ async function executeLiveCapture(captureClient, sessionId, command, payload, timeoutMs) {
553
+ try {
554
+ return await captureClient.execute(sessionId, command, payload, timeoutMs);
555
+ }
556
+ catch (error) {
557
+ throw normalizeCaptureError(sessionId, error);
558
+ }
559
+ }
560
+ function ensureCaptureSuccess(result, sessionId) {
521
561
  if (!result.ok) {
522
- throw new Error(result.error ?? 'Capture command failed');
562
+ throw normalizeCaptureError(sessionId, new Error(result.error ?? 'Capture command failed'));
523
563
  }
524
564
  return result.payload ?? {};
525
565
  }
526
- export function createV1ToolHandlers(getDb) {
566
+ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
527
567
  return {
528
568
  list_sessions: async (input) => {
529
569
  const db = getDb();
@@ -577,6 +617,23 @@ export function createV1ToolHandlers(getDb) {
577
617
  dpr: row.dpr ?? undefined,
578
618
  safeMode: row.safe_mode === 1,
579
619
  pinned: row.pinned === 1,
620
+ liveConnection: (() => {
621
+ const state = getSessionConnectionState?.(row.session_id);
622
+ if (!state) {
623
+ return {
624
+ connected: false,
625
+ lastHeartbeatAt: undefined,
626
+ disconnectReason: row.ended_at ? 'manual_stop' : undefined,
627
+ };
628
+ }
629
+ return {
630
+ connected: state.connected,
631
+ connectedAt: state.connectedAt,
632
+ lastHeartbeatAt: state.lastHeartbeatAt,
633
+ disconnectedAt: state.disconnectedAt,
634
+ disconnectReason: state.disconnectReason,
635
+ };
636
+ })(),
580
637
  }));
581
638
  return {
582
639
  ...createBaseResponse(),
@@ -1373,14 +1430,14 @@ export function createV2ToolHandlers(captureClient) {
1373
1430
  }
1374
1431
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1375
1432
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1376
- const capture = await captureClient.execute(sessionId, 'CAPTURE_DOM_SUBTREE', { selector, maxDepth, maxBytes }, 4_000);
1433
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_SUBTREE', { selector, maxDepth, maxBytes }, 4_000);
1377
1434
  return {
1378
1435
  ...createBaseResponse(sessionId),
1379
1436
  limitsApplied: {
1380
1437
  maxResults: maxBytes,
1381
1438
  truncated: capture.truncated ?? false,
1382
1439
  },
1383
- ...ensureCaptureSuccess(capture),
1440
+ ...ensureCaptureSuccess(capture, sessionId),
1384
1441
  };
1385
1442
  },
1386
1443
  get_dom_document: async (input) => {
@@ -1392,21 +1449,22 @@ export function createV2ToolHandlers(captureClient) {
1392
1449
  const maxBytes = resolveCaptureBytes(input.maxBytes, 200_000);
1393
1450
  const maxDepth = resolveCaptureDepth(input.maxDepth, 4);
1394
1451
  try {
1395
- const capture = await captureClient.execute(sessionId, 'CAPTURE_DOM_DOCUMENT', { mode, maxBytes, maxDepth }, 4_000);
1452
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_DOCUMENT', { mode, maxBytes, maxDepth }, 4_000);
1396
1453
  return {
1397
1454
  ...createBaseResponse(sessionId),
1398
1455
  limitsApplied: {
1399
1456
  maxResults: maxBytes,
1400
1457
  truncated: capture.truncated ?? false,
1401
1458
  },
1402
- ...ensureCaptureSuccess(capture),
1459
+ ...ensureCaptureSuccess(capture, sessionId),
1403
1460
  };
1404
1461
  }
1405
1462
  catch (error) {
1406
- if (mode !== 'html') {
1407
- throw error;
1463
+ const normalized = normalizeCaptureError(sessionId, error);
1464
+ if (mode !== 'html' || isLiveSessionDisconnectedError(normalized)) {
1465
+ throw normalized;
1408
1466
  }
1409
- const fallback = await captureClient.execute(sessionId, 'CAPTURE_DOM_DOCUMENT', { mode: 'outline', maxBytes, maxDepth }, 4_000);
1467
+ const fallback = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_DOCUMENT', { mode: 'outline', maxBytes, maxDepth }, 4_000);
1410
1468
  return {
1411
1469
  ...createBaseResponse(sessionId),
1412
1470
  limitsApplied: {
@@ -1414,7 +1472,7 @@ export function createV2ToolHandlers(captureClient) {
1414
1472
  truncated: true,
1415
1473
  },
1416
1474
  fallbackReason: 'timeout',
1417
- ...ensureCaptureSuccess(fallback),
1475
+ ...ensureCaptureSuccess(fallback, sessionId),
1418
1476
  };
1419
1477
  }
1420
1478
  },
@@ -1428,14 +1486,14 @@ export function createV2ToolHandlers(captureClient) {
1428
1486
  throw new Error('selector is required');
1429
1487
  }
1430
1488
  const properties = asStringArray(input.properties, 64);
1431
- const capture = await captureClient.execute(sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
1489
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
1432
1490
  return {
1433
1491
  ...createBaseResponse(sessionId),
1434
1492
  limitsApplied: {
1435
1493
  maxResults: properties.length || 8,
1436
1494
  truncated: capture.truncated ?? false,
1437
1495
  },
1438
- ...ensureCaptureSuccess(capture),
1496
+ ...ensureCaptureSuccess(capture, sessionId),
1439
1497
  };
1440
1498
  },
1441
1499
  get_layout_metrics: async (input) => {
@@ -1444,14 +1502,14 @@ export function createV2ToolHandlers(captureClient) {
1444
1502
  throw new Error('sessionId is required');
1445
1503
  }
1446
1504
  const selector = typeof input.selector === 'string' ? input.selector : undefined;
1447
- const capture = await captureClient.execute(sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
1505
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
1448
1506
  return {
1449
1507
  ...createBaseResponse(sessionId),
1450
1508
  limitsApplied: {
1451
1509
  maxResults: 1,
1452
1510
  truncated: capture.truncated ?? false,
1453
1511
  },
1454
- ...ensureCaptureSuccess(capture),
1512
+ ...ensureCaptureSuccess(capture, sessionId),
1455
1513
  };
1456
1514
  },
1457
1515
  capture_ui_snapshot: async (input) => {
@@ -1473,7 +1531,7 @@ export function createV2ToolHandlers(captureClient) {
1473
1531
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1474
1532
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1475
1533
  const maxAncestors = resolveCaptureAncestors(input.maxAncestors, 4);
1476
- const capture = await captureClient.execute(sessionId, 'CAPTURE_UI_SNAPSHOT', {
1534
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
1477
1535
  selector,
1478
1536
  trigger,
1479
1537
  mode,
@@ -1490,7 +1548,7 @@ export function createV2ToolHandlers(captureClient) {
1490
1548
  maxResults: maxBytes,
1491
1549
  truncated: capture.truncated ?? false,
1492
1550
  },
1493
- ...ensureCaptureSuccess(capture),
1551
+ ...ensureCaptureSuccess(capture, sessionId),
1494
1552
  };
1495
1553
  },
1496
1554
  };
@@ -1542,7 +1600,7 @@ export function createMCPServer(overrides = {}, options = {}) {
1542
1600
  const logger = options.logger ?? createDefaultMcpLogger();
1543
1601
  const v2Handlers = options.captureClient ? createV2ToolHandlers(options.captureClient) : {};
1544
1602
  const tools = createToolRegistry({
1545
- ...createV1ToolHandlers(() => getConnection().db),
1603
+ ...createV1ToolHandlers(() => getConnection().db, options.getSessionConnectionState),
1546
1604
  ...v2Handlers,
1547
1605
  ...overrides,
1548
1606
  });