browser-debug-mcp-bridge 1.4.0 → 1.6.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.
@@ -38,9 +38,9 @@ const TOOL_SCHEMAS = {
38
38
  },
39
39
  get_recent_events: {
40
40
  type: 'object',
41
- required: ['sessionId'],
42
41
  properties: {
43
42
  sessionId: { type: 'string' },
43
+ url: { type: 'string' },
44
44
  eventTypes: { type: 'array', items: { type: 'string' } },
45
45
  limit: { type: 'number' },
46
46
  offset: { type: 'number' },
@@ -48,18 +48,18 @@ const TOOL_SCHEMAS = {
48
48
  },
49
49
  get_navigation_history: {
50
50
  type: 'object',
51
- required: ['sessionId'],
52
51
  properties: {
53
52
  sessionId: { type: 'string' },
53
+ url: { type: 'string' },
54
54
  limit: { type: 'number' },
55
55
  offset: { type: 'number' },
56
56
  },
57
57
  },
58
58
  get_console_events: {
59
59
  type: 'object',
60
- required: ['sessionId'],
61
60
  properties: {
62
61
  sessionId: { type: 'string' },
62
+ url: { type: 'string' },
63
63
  level: { type: 'string' },
64
64
  limit: { type: 'number' },
65
65
  offset: { type: 'number' },
@@ -78,6 +78,7 @@ const TOOL_SCHEMAS = {
78
78
  type: 'object',
79
79
  properties: {
80
80
  sessionId: { type: 'string' },
81
+ url: { type: 'string' },
81
82
  errorType: { type: 'string' },
82
83
  groupBy: { type: 'string' },
83
84
  limit: { type: 'number' },
@@ -143,6 +144,20 @@ const TOOL_SCHEMAS = {
143
144
  maxAncestors: { type: 'number' },
144
145
  },
145
146
  },
147
+ get_live_console_logs: {
148
+ type: 'object',
149
+ required: ['sessionId'],
150
+ properties: {
151
+ sessionId: { type: 'string' },
152
+ url: { type: 'string' },
153
+ tabId: { type: 'number' },
154
+ levels: { type: 'array', items: { type: 'string' } },
155
+ contains: { type: 'string' },
156
+ sinceTs: { type: 'number' },
157
+ includeRuntimeErrors: { type: 'boolean' },
158
+ limit: { type: 'number' },
159
+ },
160
+ },
146
161
  explain_last_failure: {
147
162
  type: 'object',
148
163
  required: ['sessionId'],
@@ -208,6 +223,7 @@ const TOOL_DESCRIPTIONS = {
208
223
  get_computed_styles: 'Read computed CSS styles for an element',
209
224
  get_layout_metrics: 'Read viewport and element layout metrics',
210
225
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
226
+ get_live_console_logs: 'Read in-memory live console logs for a connected session',
211
227
  explain_last_failure: 'Explain the latest failure timeline',
212
228
  get_event_correlation: 'Correlate related events by window',
213
229
  list_snapshots: 'List snapshot metadata by session/time/trigger',
@@ -225,6 +241,7 @@ const DEFAULT_EVENT_LIMIT = 50;
225
241
  const MAX_LIMIT = 200;
226
242
  const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
227
243
  const MAX_SNAPSHOT_ASSET_CHUNK_BYTES = 256 * 1024;
244
+ const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
228
245
  const NETWORK_DOMAIN_GROUP_SQL = `
229
246
  CASE
230
247
  WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
@@ -236,6 +253,16 @@ const NETWORK_DOMAIN_GROUP_SQL = `
236
253
  ELSE replace(replace(url, 'https://', ''), 'http://', '')
237
254
  END
238
255
  `;
256
+ class LiveSessionDisconnectedError extends Error {
257
+ code = LIVE_SESSION_DISCONNECTED_CODE;
258
+ constructor(sessionId, reason) {
259
+ const normalizedReason = typeof reason === 'string' && reason.trim().length > 0
260
+ ? reason.trim()
261
+ : 'Extension connection is stale or unavailable';
262
+ 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.`);
263
+ this.name = 'LiveSessionDisconnectedError';
264
+ }
265
+ }
239
266
  function resolveLimit(value, fallback) {
240
267
  if (typeof value !== 'number' || !Number.isFinite(value)) {
241
268
  return fallback;
@@ -292,6 +319,65 @@ function parseRequestedTypes(value) {
292
319
  .map((entry) => mapRequestedEventType(entry));
293
320
  return Array.from(new Set(normalized));
294
321
  }
322
+ function normalizeRequestedOrigin(value) {
323
+ if (value === undefined || value === null || value === '') {
324
+ return undefined;
325
+ }
326
+ if (typeof value !== 'string') {
327
+ throw new Error('url must be a string');
328
+ }
329
+ try {
330
+ const parsed = new URL(value);
331
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
332
+ throw new Error('url must use http:// or https://');
333
+ }
334
+ return parsed.origin;
335
+ }
336
+ catch {
337
+ throw new Error('url must be a valid absolute http(s) URL');
338
+ }
339
+ }
340
+ function ensureSessionOrOriginFilter(sessionId, origin) {
341
+ if (!sessionId && !origin) {
342
+ throw new Error('sessionId or url is required');
343
+ }
344
+ }
345
+ function resolveUrlPrefixFromOrigin(origin) {
346
+ return origin.endsWith('/') ? origin : origin + '/';
347
+ }
348
+ function appendEventOriginFilter(where, params, origin) {
349
+ if (!origin) {
350
+ return;
351
+ }
352
+ const prefix = resolveUrlPrefixFromOrigin(origin);
353
+ where.push(`
354
+ (
355
+ origin = ?
356
+ OR (
357
+ origin IS NULL AND (
358
+ json_extract(payload_json, '$.origin') = ?
359
+ OR json_extract(payload_json, '$.url') = ?
360
+ OR json_extract(payload_json, '$.url') LIKE ?
361
+ OR json_extract(payload_json, '$.to') = ?
362
+ OR json_extract(payload_json, '$.to') LIKE ?
363
+ OR json_extract(payload_json, '$.href') = ?
364
+ OR json_extract(payload_json, '$.href') LIKE ?
365
+ OR json_extract(payload_json, '$.location') = ?
366
+ OR json_extract(payload_json, '$.location') LIKE ?
367
+ )
368
+ )
369
+ )
370
+ `);
371
+ params.push(origin, origin, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`);
372
+ }
373
+ function appendNetworkOriginFilter(where, params, origin) {
374
+ if (!origin) {
375
+ return;
376
+ }
377
+ const prefix = resolveUrlPrefixFromOrigin(origin);
378
+ where.push('(origin = ? OR (origin IS NULL AND (url = ? OR url LIKE ?)))');
379
+ params.push(origin, origin, `${prefix}%`);
380
+ }
295
381
  function resolveLastUrl(payload) {
296
382
  const candidates = [payload.url, payload.to, payload.href, payload.location];
297
383
  for (const candidate of candidates) {
@@ -302,12 +388,17 @@ function resolveLastUrl(payload) {
302
388
  return undefined;
303
389
  }
304
390
  function mapEventRecord(row) {
391
+ const payload = readJsonPayload(row.payload_json);
305
392
  return {
306
393
  eventId: row.event_id,
307
394
  sessionId: row.session_id,
308
395
  timestamp: row.ts,
309
396
  type: row.type,
310
- payload: readJsonPayload(row.payload_json),
397
+ tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
398
+ origin: row.origin
399
+ ?? (typeof payload.origin === 'string' ? payload.origin : undefined)
400
+ ?? undefined,
401
+ payload,
311
402
  };
312
403
  }
313
404
  function classifyNetworkFailure(status, errorClass) {
@@ -517,13 +608,62 @@ function asStringArray(value, maxItems) {
517
608
  .filter((entry) => typeof entry === 'string' && entry.length > 0)
518
609
  .slice(0, maxItems);
519
610
  }
520
- function ensureCaptureSuccess(result) {
611
+ const LIVE_CONSOLE_LEVELS = new Set(['log', 'info', 'warn', 'error', 'debug', 'trace']);
612
+ function resolveLiveConsoleLevels(value) {
613
+ const levels = asStringArray(value, 16)
614
+ .map((entry) => entry.trim().toLowerCase())
615
+ .filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
616
+ return Array.from(new Set(levels));
617
+ }
618
+ function resolveOptionalTabId(value) {
619
+ if (value === undefined || value === null || value === '') {
620
+ return undefined;
621
+ }
622
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
623
+ throw new Error('tabId must be an integer');
624
+ }
625
+ const tabId = Math.floor(value);
626
+ if (!Number.isInteger(tabId) || tabId < 0) {
627
+ throw new Error('tabId must be an integer');
628
+ }
629
+ return tabId;
630
+ }
631
+ function isLiveSessionDisconnectedMessage(message) {
632
+ const normalized = message.toLowerCase();
633
+ return normalized.includes('no active extension connection')
634
+ || normalized.includes('receiving end does not exist')
635
+ || normalized.includes('could not establish connection')
636
+ || normalized.includes('connection closed before capture completed')
637
+ || normalized.includes('websocket manager closed')
638
+ || normalized.includes('extension target is unavailable')
639
+ || normalized.includes('target tab for this session is unavailable');
640
+ }
641
+ function normalizeCaptureError(sessionId, error) {
642
+ const fallback = error instanceof Error ? error : new Error(String(error));
643
+ const message = fallback.message ?? '';
644
+ if (isLiveSessionDisconnectedMessage(message)) {
645
+ return new LiveSessionDisconnectedError(sessionId, message);
646
+ }
647
+ return fallback;
648
+ }
649
+ function isLiveSessionDisconnectedError(error) {
650
+ return error instanceof LiveSessionDisconnectedError;
651
+ }
652
+ async function executeLiveCapture(captureClient, sessionId, command, payload, timeoutMs) {
653
+ try {
654
+ return await captureClient.execute(sessionId, command, payload, timeoutMs);
655
+ }
656
+ catch (error) {
657
+ throw normalizeCaptureError(sessionId, error);
658
+ }
659
+ }
660
+ function ensureCaptureSuccess(result, sessionId) {
521
661
  if (!result.ok) {
522
- throw new Error(result.error ?? 'Capture command failed');
662
+ throw normalizeCaptureError(sessionId, new Error(result.error ?? 'Capture command failed'));
523
663
  }
524
664
  return result.payload ?? {};
525
665
  }
526
- export function createV1ToolHandlers(getDb) {
666
+ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
527
667
  return {
528
668
  list_sessions: async (input) => {
529
669
  const db = getDb();
@@ -577,6 +717,23 @@ export function createV1ToolHandlers(getDb) {
577
717
  dpr: row.dpr ?? undefined,
578
718
  safeMode: row.safe_mode === 1,
579
719
  pinned: row.pinned === 1,
720
+ liveConnection: (() => {
721
+ const state = getSessionConnectionState?.(row.session_id);
722
+ if (!state) {
723
+ return {
724
+ connected: false,
725
+ lastHeartbeatAt: undefined,
726
+ disconnectReason: row.ended_at ? 'manual_stop' : undefined,
727
+ };
728
+ }
729
+ return {
730
+ connected: state.connected,
731
+ connectedAt: state.connectedAt,
732
+ lastHeartbeatAt: state.lastHeartbeatAt,
733
+ disconnectedAt: state.disconnectedAt,
734
+ disconnectReason: state.disconnectReason,
735
+ };
736
+ })(),
580
737
  }));
581
738
  return {
582
739
  ...createBaseResponse(),
@@ -656,14 +813,18 @@ export function createV1ToolHandlers(getDb) {
656
813
  get_recent_events: async (input) => {
657
814
  const db = getDb();
658
815
  const sessionId = getSessionId(input);
659
- if (!sessionId) {
660
- throw new Error('sessionId is required');
661
- }
816
+ const origin = normalizeRequestedOrigin(input.url);
817
+ ensureSessionOrOriginFilter(sessionId, origin);
662
818
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
663
819
  const offset = resolveOffset(input.offset);
664
820
  const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
665
- const params = [sessionId];
666
- const where = ['session_id = ?'];
821
+ const params = [];
822
+ const where = [];
823
+ if (sessionId) {
824
+ where.push('session_id = ?');
825
+ params.push(sessionId);
826
+ }
827
+ appendEventOriginFilter(where, params, origin);
667
828
  if (requestedTypes.length > 0) {
668
829
  const placeholders = requestedTypes.map(() => '?').join(', ');
669
830
  where.push(`type IN (${placeholders})`);
@@ -671,7 +832,7 @@ export function createV1ToolHandlers(getDb) {
671
832
  }
672
833
  const rows = db
673
834
  .prepare(`
674
- SELECT event_id, session_id, ts, type, payload_json
835
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
675
836
  FROM events
676
837
  WHERE ${where.join(' AND ')}
677
838
  ORDER BY ts DESC
@@ -695,20 +856,26 @@ export function createV1ToolHandlers(getDb) {
695
856
  get_navigation_history: async (input) => {
696
857
  const db = getDb();
697
858
  const sessionId = getSessionId(input);
698
- if (!sessionId) {
699
- throw new Error('sessionId is required');
700
- }
859
+ const origin = normalizeRequestedOrigin(input.url);
860
+ ensureSessionOrOriginFilter(sessionId, origin);
701
861
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
702
862
  const offset = resolveOffset(input.offset);
863
+ const params = [];
864
+ const where = ["type = 'nav'"];
865
+ if (sessionId) {
866
+ where.push('session_id = ?');
867
+ params.push(sessionId);
868
+ }
869
+ appendEventOriginFilter(where, params, origin);
703
870
  const rows = db
704
871
  .prepare(`
705
- SELECT event_id, session_id, ts, type, payload_json
872
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
706
873
  FROM events
707
- WHERE session_id = ? AND type = 'nav'
874
+ WHERE ${where.join(' AND ')}
708
875
  ORDER BY ts DESC
709
876
  LIMIT ? OFFSET ?
710
877
  `)
711
- .all(sessionId, limit + 1, offset);
878
+ .all(...params, limit + 1, offset);
712
879
  const truncated = rows.length > limit;
713
880
  return {
714
881
  ...createBaseResponse(sessionId),
@@ -726,23 +893,27 @@ export function createV1ToolHandlers(getDb) {
726
893
  get_console_events: async (input) => {
727
894
  const db = getDb();
728
895
  const sessionId = getSessionId(input);
729
- if (!sessionId) {
730
- throw new Error('sessionId is required');
731
- }
896
+ const origin = normalizeRequestedOrigin(input.url);
897
+ ensureSessionOrOriginFilter(sessionId, origin);
732
898
  const level = typeof input.level === 'string' ? input.level : undefined;
733
899
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
734
900
  const offset = resolveOffset(input.offset);
735
- const params = [sessionId];
736
- let levelClause = '';
901
+ const params = [];
902
+ const where = ["type = 'console'"];
903
+ if (sessionId) {
904
+ where.push('session_id = ?');
905
+ params.push(sessionId);
906
+ }
907
+ appendEventOriginFilter(where, params, origin);
737
908
  if (level) {
738
- levelClause = "AND json_extract(payload_json, '$.level') = ?";
909
+ where.push("json_extract(payload_json, '$.level') = ?");
739
910
  params.push(level);
740
911
  }
741
912
  const rows = db
742
913
  .prepare(`
743
- SELECT event_id, session_id, ts, type, payload_json
914
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
744
915
  FROM events
745
- WHERE session_id = ? AND type = 'console' ${levelClause}
916
+ WHERE ${where.join(' AND ')}
746
917
  ORDER BY ts DESC
747
918
  LIMIT ? OFFSET ?
748
919
  `)
@@ -814,6 +985,8 @@ export function createV1ToolHandlers(getDb) {
814
985
  get_network_failures: async (input) => {
815
986
  const db = getDb();
816
987
  const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
988
+ const origin = normalizeRequestedOrigin(input.url);
989
+ ensureSessionOrOriginFilter(sessionId, origin);
817
990
  const groupBy = typeof input.groupBy === 'string' ? input.groupBy : undefined;
818
991
  const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
819
992
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
@@ -825,6 +998,7 @@ export function createV1ToolHandlers(getDb) {
825
998
  where.push('session_id = ?');
826
999
  params.push(sessionId);
827
1000
  }
1001
+ appendNetworkOriginFilter(where, params, origin);
828
1002
  where.push(errorFilter);
829
1003
  if (errorFilter === 'error_class = ?' && errorType) {
830
1004
  params.push(errorType);
@@ -872,7 +1046,7 @@ export function createV1ToolHandlers(getDb) {
872
1046
  }
873
1047
  const rows = db
874
1048
  .prepare(`
875
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1049
+ SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
876
1050
  FROM network
877
1051
  ${whereClause}
878
1052
  ORDER BY ts_start DESC
@@ -897,6 +1071,7 @@ export function createV1ToolHandlers(getDb) {
897
1071
  durationMs: row.duration_ms ?? undefined,
898
1072
  method: row.method,
899
1073
  url: row.url,
1074
+ origin: row.origin ?? undefined,
900
1075
  status: row.status ?? undefined,
901
1076
  initiator: row.initiator ?? undefined,
902
1077
  errorType: classifyNetworkFailure(row.status, row.error_class),
@@ -917,7 +1092,7 @@ export function createV1ToolHandlers(getDb) {
917
1092
  const offset = resolveOffset(input.offset);
918
1093
  const rows = db
919
1094
  .prepare(`
920
- SELECT event_id, session_id, ts, type, payload_json
1095
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
921
1096
  FROM events
922
1097
  WHERE session_id = ?
923
1098
  AND type IN ('ui', 'element_ref')
@@ -951,7 +1126,7 @@ export function createV1ToolHandlers(getDb) {
951
1126
  const windowMs = lookbackSeconds * 1000;
952
1127
  const latestErrorEvent = db
953
1128
  .prepare(`
954
- SELECT event_id, session_id, ts, type, payload_json
1129
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
955
1130
  FROM events
956
1131
  WHERE session_id = ?
957
1132
  AND (type = 'error' OR (type = 'console' AND json_extract(payload_json, '$.level') = 'error'))
@@ -961,7 +1136,7 @@ export function createV1ToolHandlers(getDb) {
961
1136
  .get(sessionId);
962
1137
  const latestNetworkFailure = db
963
1138
  .prepare(`
964
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1139
+ SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
965
1140
  FROM network
966
1141
  WHERE session_id = ?
967
1142
  AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
@@ -989,7 +1164,7 @@ export function createV1ToolHandlers(getDb) {
989
1164
  const windowEnd = anchorTs + 1_000;
990
1165
  const eventRows = db
991
1166
  .prepare(`
992
- SELECT event_id, session_id, ts, type, payload_json
1167
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
993
1168
  FROM events
994
1169
  WHERE session_id = ?
995
1170
  AND ts BETWEEN ? AND ?
@@ -998,7 +1173,7 @@ export function createV1ToolHandlers(getDb) {
998
1173
  .all(sessionId, windowStart, windowEnd);
999
1174
  const networkRows = db
1000
1175
  .prepare(`
1001
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1176
+ SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1002
1177
  FROM network
1003
1178
  WHERE session_id = ?
1004
1179
  AND ts_start BETWEEN ? AND ?
@@ -1081,7 +1256,7 @@ export function createV1ToolHandlers(getDb) {
1081
1256
  }
1082
1257
  const anchorEvent = db
1083
1258
  .prepare(`
1084
- SELECT event_id, session_id, ts, type, payload_json
1259
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1085
1260
  FROM events
1086
1261
  WHERE session_id = ? AND event_id = ?
1087
1262
  LIMIT 1
@@ -1096,7 +1271,7 @@ export function createV1ToolHandlers(getDb) {
1096
1271
  const windowEnd = anchorEvent.ts + windowMs;
1097
1272
  const nearbyEvents = db
1098
1273
  .prepare(`
1099
- SELECT event_id, session_id, ts, type, payload_json
1274
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1100
1275
  FROM events
1101
1276
  WHERE session_id = ?
1102
1277
  AND event_id != ?
@@ -1105,7 +1280,7 @@ export function createV1ToolHandlers(getDb) {
1105
1280
  .all(sessionId, eventId, windowStart, windowEnd);
1106
1281
  const nearbyNetworkFailures = db
1107
1282
  .prepare(`
1108
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1283
+ SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1109
1284
  FROM network
1110
1285
  WHERE session_id = ?
1111
1286
  AND ts_start BETWEEN ? AND ?
@@ -1373,14 +1548,14 @@ export function createV2ToolHandlers(captureClient) {
1373
1548
  }
1374
1549
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1375
1550
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1376
- const capture = await captureClient.execute(sessionId, 'CAPTURE_DOM_SUBTREE', { selector, maxDepth, maxBytes }, 4_000);
1551
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_SUBTREE', { selector, maxDepth, maxBytes }, 4_000);
1377
1552
  return {
1378
1553
  ...createBaseResponse(sessionId),
1379
1554
  limitsApplied: {
1380
1555
  maxResults: maxBytes,
1381
1556
  truncated: capture.truncated ?? false,
1382
1557
  },
1383
- ...ensureCaptureSuccess(capture),
1558
+ ...ensureCaptureSuccess(capture, sessionId),
1384
1559
  };
1385
1560
  },
1386
1561
  get_dom_document: async (input) => {
@@ -1392,21 +1567,22 @@ export function createV2ToolHandlers(captureClient) {
1392
1567
  const maxBytes = resolveCaptureBytes(input.maxBytes, 200_000);
1393
1568
  const maxDepth = resolveCaptureDepth(input.maxDepth, 4);
1394
1569
  try {
1395
- const capture = await captureClient.execute(sessionId, 'CAPTURE_DOM_DOCUMENT', { mode, maxBytes, maxDepth }, 4_000);
1570
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_DOCUMENT', { mode, maxBytes, maxDepth }, 4_000);
1396
1571
  return {
1397
1572
  ...createBaseResponse(sessionId),
1398
1573
  limitsApplied: {
1399
1574
  maxResults: maxBytes,
1400
1575
  truncated: capture.truncated ?? false,
1401
1576
  },
1402
- ...ensureCaptureSuccess(capture),
1577
+ ...ensureCaptureSuccess(capture, sessionId),
1403
1578
  };
1404
1579
  }
1405
1580
  catch (error) {
1406
- if (mode !== 'html') {
1407
- throw error;
1581
+ const normalized = normalizeCaptureError(sessionId, error);
1582
+ if (mode !== 'html' || isLiveSessionDisconnectedError(normalized)) {
1583
+ throw normalized;
1408
1584
  }
1409
- const fallback = await captureClient.execute(sessionId, 'CAPTURE_DOM_DOCUMENT', { mode: 'outline', maxBytes, maxDepth }, 4_000);
1585
+ const fallback = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_DOM_DOCUMENT', { mode: 'outline', maxBytes, maxDepth }, 4_000);
1410
1586
  return {
1411
1587
  ...createBaseResponse(sessionId),
1412
1588
  limitsApplied: {
@@ -1414,7 +1590,7 @@ export function createV2ToolHandlers(captureClient) {
1414
1590
  truncated: true,
1415
1591
  },
1416
1592
  fallbackReason: 'timeout',
1417
- ...ensureCaptureSuccess(fallback),
1593
+ ...ensureCaptureSuccess(fallback, sessionId),
1418
1594
  };
1419
1595
  }
1420
1596
  },
@@ -1428,14 +1604,14 @@ export function createV2ToolHandlers(captureClient) {
1428
1604
  throw new Error('selector is required');
1429
1605
  }
1430
1606
  const properties = asStringArray(input.properties, 64);
1431
- const capture = await captureClient.execute(sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
1607
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_COMPUTED_STYLES', { selector, properties }, 3_000);
1432
1608
  return {
1433
1609
  ...createBaseResponse(sessionId),
1434
1610
  limitsApplied: {
1435
1611
  maxResults: properties.length || 8,
1436
1612
  truncated: capture.truncated ?? false,
1437
1613
  },
1438
- ...ensureCaptureSuccess(capture),
1614
+ ...ensureCaptureSuccess(capture, sessionId),
1439
1615
  };
1440
1616
  },
1441
1617
  get_layout_metrics: async (input) => {
@@ -1444,14 +1620,14 @@ export function createV2ToolHandlers(captureClient) {
1444
1620
  throw new Error('sessionId is required');
1445
1621
  }
1446
1622
  const selector = typeof input.selector === 'string' ? input.selector : undefined;
1447
- const capture = await captureClient.execute(sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
1623
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_LAYOUT_METRICS', { selector }, 3_000);
1448
1624
  return {
1449
1625
  ...createBaseResponse(sessionId),
1450
1626
  limitsApplied: {
1451
1627
  maxResults: 1,
1452
1628
  truncated: capture.truncated ?? false,
1453
1629
  },
1454
- ...ensureCaptureSuccess(capture),
1630
+ ...ensureCaptureSuccess(capture, sessionId),
1455
1631
  };
1456
1632
  },
1457
1633
  capture_ui_snapshot: async (input) => {
@@ -1473,7 +1649,7 @@ export function createV2ToolHandlers(captureClient) {
1473
1649
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1474
1650
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1475
1651
  const maxAncestors = resolveCaptureAncestors(input.maxAncestors, 4);
1476
- const capture = await captureClient.execute(sessionId, 'CAPTURE_UI_SNAPSHOT', {
1652
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
1477
1653
  selector,
1478
1654
  trigger,
1479
1655
  mode,
@@ -1490,7 +1666,39 @@ export function createV2ToolHandlers(captureClient) {
1490
1666
  maxResults: maxBytes,
1491
1667
  truncated: capture.truncated ?? false,
1492
1668
  },
1493
- ...ensureCaptureSuccess(capture),
1669
+ ...ensureCaptureSuccess(capture, sessionId),
1670
+ };
1671
+ },
1672
+ get_live_console_logs: async (input) => {
1673
+ const sessionId = getSessionId(input);
1674
+ if (!sessionId) {
1675
+ throw new Error('sessionId is required');
1676
+ }
1677
+ const origin = normalizeRequestedOrigin(input.url);
1678
+ const tabId = resolveOptionalTabId(input.tabId);
1679
+ const levels = resolveLiveConsoleLevels(input.levels);
1680
+ const contains = typeof input.contains === 'string' && input.contains.trim().length > 0
1681
+ ? input.contains.trim()
1682
+ : undefined;
1683
+ const sinceTs = resolveOptionalTimestamp(input.sinceTs);
1684
+ const includeRuntimeErrors = input.includeRuntimeErrors !== false;
1685
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
1686
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
1687
+ origin,
1688
+ tabId,
1689
+ levels,
1690
+ contains,
1691
+ sinceTs,
1692
+ includeRuntimeErrors,
1693
+ limit,
1694
+ }, 3_000);
1695
+ return {
1696
+ ...createBaseResponse(sessionId),
1697
+ limitsApplied: {
1698
+ maxResults: limit,
1699
+ truncated: capture.truncated ?? false,
1700
+ },
1701
+ ...ensureCaptureSuccess(capture, sessionId),
1494
1702
  };
1495
1703
  },
1496
1704
  };
@@ -1542,7 +1750,7 @@ export function createMCPServer(overrides = {}, options = {}) {
1542
1750
  const logger = options.logger ?? createDefaultMcpLogger();
1543
1751
  const v2Handlers = options.captureClient ? createV2ToolHandlers(options.captureClient) : {};
1544
1752
  const tools = createToolRegistry({
1545
- ...createV1ToolHandlers(() => getConnection().db),
1753
+ ...createV1ToolHandlers(() => getConnection().db, options.getSessionConnectionState),
1546
1754
  ...v2Handlers,
1547
1755
  ...overrides,
1548
1756
  });