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.
@@ -27,6 +27,7 @@ const TOOL_SCHEMAS = {
27
27
  sinceMinutes: { type: 'number' },
28
28
  limit: { type: 'number' },
29
29
  offset: { type: 'number' },
30
+ maxResponseBytes: { type: 'number' },
30
31
  },
31
32
  },
32
33
  get_session_summary: {
@@ -38,31 +39,60 @@ const TOOL_SCHEMAS = {
38
39
  },
39
40
  get_recent_events: {
40
41
  type: 'object',
41
- required: ['sessionId'],
42
42
  properties: {
43
43
  sessionId: { type: 'string' },
44
+ url: { type: 'string' },
44
45
  eventTypes: { type: 'array', items: { type: 'string' } },
45
46
  limit: { type: 'number' },
46
47
  offset: { type: 'number' },
48
+ responseProfile: { type: 'string' },
49
+ includePayload: { type: 'boolean' },
50
+ maxResponseBytes: { type: 'number' },
47
51
  },
48
52
  },
49
53
  get_navigation_history: {
50
54
  type: 'object',
51
- required: ['sessionId'],
52
55
  properties: {
53
56
  sessionId: { type: 'string' },
57
+ url: { type: 'string' },
54
58
  limit: { type: 'number' },
55
59
  offset: { type: 'number' },
60
+ responseProfile: { type: 'string' },
61
+ includePayload: { type: 'boolean' },
62
+ maxResponseBytes: { type: 'number' },
56
63
  },
57
64
  },
58
65
  get_console_events: {
59
66
  type: 'object',
60
- required: ['sessionId'],
61
67
  properties: {
62
68
  sessionId: { type: 'string' },
69
+ url: { type: 'string' },
63
70
  level: { type: 'string' },
64
71
  limit: { type: 'number' },
65
72
  offset: { type: 'number' },
73
+ responseProfile: { type: 'string' },
74
+ includePayload: { type: 'boolean' },
75
+ maxResponseBytes: { type: 'number' },
76
+ },
77
+ },
78
+ get_console_summary: {
79
+ type: 'object',
80
+ properties: {
81
+ sessionId: { type: 'string' },
82
+ url: { type: 'string' },
83
+ level: { type: 'string' },
84
+ sinceMinutes: { type: 'number' },
85
+ limit: { type: 'number' },
86
+ },
87
+ },
88
+ get_event_summary: {
89
+ type: 'object',
90
+ properties: {
91
+ sessionId: { type: 'string' },
92
+ url: { type: 'string' },
93
+ eventTypes: { type: 'array', items: { type: 'string' } },
94
+ sinceMinutes: { type: 'number' },
95
+ limit: { type: 'number' },
66
96
  },
67
97
  },
68
98
  get_error_fingerprints: {
@@ -72,16 +102,68 @@ const TOOL_SCHEMAS = {
72
102
  sinceMinutes: { type: 'number' },
73
103
  limit: { type: 'number' },
74
104
  offset: { type: 'number' },
105
+ maxResponseBytes: { type: 'number' },
75
106
  },
76
107
  },
77
108
  get_network_failures: {
78
109
  type: 'object',
79
110
  properties: {
80
111
  sessionId: { type: 'string' },
112
+ url: { type: 'string' },
81
113
  errorType: { type: 'string' },
82
114
  groupBy: { type: 'string' },
83
115
  limit: { type: 'number' },
84
116
  offset: { type: 'number' },
117
+ maxResponseBytes: { type: 'number' },
118
+ },
119
+ },
120
+ get_network_calls: {
121
+ type: 'object',
122
+ required: ['sessionId'],
123
+ properties: {
124
+ sessionId: { type: 'string' },
125
+ urlContains: { type: 'string' },
126
+ urlRegex: { type: 'string' },
127
+ method: { type: 'string' },
128
+ statusIn: { type: 'array', items: { type: 'number' } },
129
+ tabId: { type: 'number' },
130
+ timeFrom: { type: 'number' },
131
+ timeTo: { type: 'number' },
132
+ includeBodies: { type: 'boolean' },
133
+ limit: { type: 'number' },
134
+ offset: { type: 'number' },
135
+ maxResponseBytes: { type: 'number' },
136
+ },
137
+ },
138
+ wait_for_network_call: {
139
+ type: 'object',
140
+ required: ['sessionId', 'urlPattern'],
141
+ properties: {
142
+ sessionId: { type: 'string' },
143
+ urlPattern: { type: 'string' },
144
+ method: { type: 'string' },
145
+ timeoutMs: { type: 'number' },
146
+ includeBodies: { type: 'boolean' },
147
+ },
148
+ },
149
+ get_request_trace: {
150
+ type: 'object',
151
+ properties: {
152
+ sessionId: { type: 'string' },
153
+ requestId: { type: 'string' },
154
+ traceId: { type: 'string' },
155
+ includeBodies: { type: 'boolean' },
156
+ eventLimit: { type: 'number' },
157
+ },
158
+ },
159
+ get_body_chunk: {
160
+ type: 'object',
161
+ required: ['chunkRef'],
162
+ properties: {
163
+ chunkRef: { type: 'string' },
164
+ sessionId: { type: 'string' },
165
+ offset: { type: 'number' },
166
+ limit: { type: 'number' },
85
167
  },
86
168
  },
87
169
  get_element_refs: {
@@ -92,6 +174,7 @@ const TOOL_SCHEMAS = {
92
174
  selector: { type: 'string' },
93
175
  limit: { type: 'number' },
94
176
  offset: { type: 'number' },
177
+ maxResponseBytes: { type: 'number' },
95
178
  },
96
179
  },
97
180
  get_dom_subtree: {
@@ -141,6 +224,27 @@ const TOOL_SCHEMAS = {
141
224
  maxDepth: { type: 'number' },
142
225
  maxBytes: { type: 'number' },
143
226
  maxAncestors: { type: 'number' },
227
+ includeDom: { type: 'boolean' },
228
+ includeStyles: { type: 'boolean' },
229
+ includePngDataUrl: { type: 'boolean' },
230
+ },
231
+ },
232
+ get_live_console_logs: {
233
+ type: 'object',
234
+ required: ['sessionId'],
235
+ properties: {
236
+ sessionId: { type: 'string' },
237
+ url: { type: 'string' },
238
+ tabId: { type: 'number' },
239
+ levels: { type: 'array', items: { type: 'string' } },
240
+ contains: { type: 'string' },
241
+ sinceTs: { type: 'number' },
242
+ includeRuntimeErrors: { type: 'boolean' },
243
+ dedupeWindowMs: { type: 'number' },
244
+ limit: { type: 'number' },
245
+ responseProfile: { type: 'string' },
246
+ includeArgs: { type: 'boolean' },
247
+ maxResponseBytes: { type: 'number' },
144
248
  },
145
249
  },
146
250
  explain_last_failure: {
@@ -170,6 +274,7 @@ const TOOL_SCHEMAS = {
170
274
  untilTimestamp: { type: 'number' },
171
275
  limit: { type: 'number' },
172
276
  offset: { type: 'number' },
277
+ maxResponseBytes: { type: 'number' },
173
278
  },
174
279
  },
175
280
  get_snapshot_for_event: {
@@ -200,14 +305,21 @@ const TOOL_DESCRIPTIONS = {
200
305
  get_recent_events: 'Read recent events from a session',
201
306
  get_navigation_history: 'Read navigation events for a session',
202
307
  get_console_events: 'Read console events for a session',
308
+ get_console_summary: 'Summarize console volume and top repeated messages',
309
+ get_event_summary: 'Summarize event volume and type distribution',
203
310
  get_error_fingerprints: 'List aggregated error fingerprints',
204
311
  get_network_failures: 'List network failures and groupings',
312
+ get_network_calls: 'Query network calls with targeted filters and optional sanitized bodies',
313
+ wait_for_network_call: 'Wait for the next matching network call and return it deterministically',
314
+ get_request_trace: 'Get correlated UI/events/network chain by requestId or traceId',
315
+ get_body_chunk: 'Retrieve a chunk from a stored large body payload',
205
316
  get_element_refs: 'Get element references by selector',
206
317
  get_dom_subtree: 'Capture a bounded DOM subtree',
207
318
  get_dom_document: 'Capture full document as outline or html',
208
319
  get_computed_styles: 'Read computed CSS styles for an element',
209
320
  get_layout_metrics: 'Read viewport and element layout metrics',
210
321
  capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
322
+ get_live_console_logs: 'Read in-memory live console logs for a connected session',
211
323
  explain_last_failure: 'Explain the latest failure timeline',
212
324
  get_event_correlation: 'Correlate related events by window',
213
325
  list_snapshots: 'List snapshot metadata by session/time/trigger',
@@ -223,9 +335,21 @@ const DEFAULT_REDACTION_SUMMARY = {
223
335
  const DEFAULT_LIST_LIMIT = 25;
224
336
  const DEFAULT_EVENT_LIMIT = 50;
225
337
  const MAX_LIMIT = 200;
338
+ const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
339
+ const MAX_RESPONSE_BYTES = 512 * 1024;
226
340
  const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
227
341
  const MAX_SNAPSHOT_ASSET_CHUNK_BYTES = 256 * 1024;
342
+ const DEFAULT_BODY_CHUNK_BYTES = 64 * 1024;
343
+ const MAX_BODY_CHUNK_BYTES = 256 * 1024;
344
+ const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
345
+ const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
346
+ const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
228
347
  const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
348
+ const NETWORK_CALL_SELECT_COLUMNS = `
349
+ request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
350
+ request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
351
+ response_content_type, response_body_text, response_body_json, response_body_bytes, response_body_truncated, response_body_chunk_ref
352
+ `;
229
353
  const NETWORK_DOMAIN_GROUP_SQL = `
230
354
  CASE
231
355
  WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
@@ -264,6 +388,62 @@ function resolveOffset(value) {
264
388
  const floored = Math.floor(value);
265
389
  return floored < 0 ? 0 : floored;
266
390
  }
391
+ function resolveResponseProfile(value) {
392
+ return value === 'compact' ? 'compact' : 'legacy';
393
+ }
394
+ function resolveMaxResponseBytes(value) {
395
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
396
+ return DEFAULT_MAX_RESPONSE_BYTES;
397
+ }
398
+ const floored = Math.floor(value);
399
+ if (floored < 1_024) {
400
+ return DEFAULT_MAX_RESPONSE_BYTES;
401
+ }
402
+ return Math.min(floored, MAX_RESPONSE_BYTES);
403
+ }
404
+ function estimateJsonBytes(value) {
405
+ return Buffer.byteLength(JSON.stringify(value), 'utf-8');
406
+ }
407
+ function applyByteBudget(items, maxResponseBytes) {
408
+ if (items.length === 0) {
409
+ return {
410
+ items: [],
411
+ responseBytes: 2, // []
412
+ truncatedByBytes: false,
413
+ };
414
+ }
415
+ const selected = [];
416
+ let usedBytes = 2; // []
417
+ let truncatedByBytes = false;
418
+ for (const item of items) {
419
+ const itemBytes = estimateJsonBytes(item);
420
+ const separatorBytes = selected.length > 0 ? 1 : 0; // comma
421
+ const nextBytes = usedBytes + separatorBytes + itemBytes;
422
+ if (nextBytes > maxResponseBytes && selected.length > 0) {
423
+ truncatedByBytes = true;
424
+ break;
425
+ }
426
+ selected.push(item);
427
+ usedBytes = nextBytes;
428
+ }
429
+ if (!truncatedByBytes && selected.length < items.length) {
430
+ truncatedByBytes = true;
431
+ }
432
+ return {
433
+ items: selected,
434
+ responseBytes: usedBytes,
435
+ truncatedByBytes,
436
+ };
437
+ }
438
+ function buildOffsetPagination(offset, returned, hasMore, maxResponseBytes) {
439
+ return {
440
+ offset,
441
+ returned,
442
+ hasMore,
443
+ nextOffset: hasMore ? offset + returned : null,
444
+ maxResponseBytes,
445
+ };
446
+ }
267
447
  function readJsonPayload(payloadJson) {
268
448
  try {
269
449
  const parsed = JSON.parse(payloadJson);
@@ -303,6 +483,65 @@ function parseRequestedTypes(value) {
303
483
  .map((entry) => mapRequestedEventType(entry));
304
484
  return Array.from(new Set(normalized));
305
485
  }
486
+ function normalizeRequestedOrigin(value) {
487
+ if (value === undefined || value === null || value === '') {
488
+ return undefined;
489
+ }
490
+ if (typeof value !== 'string') {
491
+ throw new Error('url must be a string');
492
+ }
493
+ try {
494
+ const parsed = new URL(value);
495
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
496
+ throw new Error('url must use http:// or https://');
497
+ }
498
+ return parsed.origin;
499
+ }
500
+ catch {
501
+ throw new Error('url must be a valid absolute http(s) URL');
502
+ }
503
+ }
504
+ function ensureSessionOrOriginFilter(sessionId, origin) {
505
+ if (!sessionId && !origin) {
506
+ throw new Error('sessionId or url is required');
507
+ }
508
+ }
509
+ function resolveUrlPrefixFromOrigin(origin) {
510
+ return origin.endsWith('/') ? origin : origin + '/';
511
+ }
512
+ function appendEventOriginFilter(where, params, origin) {
513
+ if (!origin) {
514
+ return;
515
+ }
516
+ const prefix = resolveUrlPrefixFromOrigin(origin);
517
+ where.push(`
518
+ (
519
+ origin = ?
520
+ OR (
521
+ origin IS NULL AND (
522
+ json_extract(payload_json, '$.origin') = ?
523
+ OR json_extract(payload_json, '$.url') = ?
524
+ OR json_extract(payload_json, '$.url') LIKE ?
525
+ OR json_extract(payload_json, '$.to') = ?
526
+ OR json_extract(payload_json, '$.to') LIKE ?
527
+ OR json_extract(payload_json, '$.href') = ?
528
+ OR json_extract(payload_json, '$.href') LIKE ?
529
+ OR json_extract(payload_json, '$.location') = ?
530
+ OR json_extract(payload_json, '$.location') LIKE ?
531
+ )
532
+ )
533
+ )
534
+ `);
535
+ params.push(origin, origin, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`, origin, `${prefix}%`);
536
+ }
537
+ function appendNetworkOriginFilter(where, params, origin) {
538
+ if (!origin) {
539
+ return;
540
+ }
541
+ const prefix = resolveUrlPrefixFromOrigin(origin);
542
+ where.push('(origin = ? OR (origin IS NULL AND (url = ? OR url LIKE ?)))');
543
+ params.push(origin, origin, `${prefix}%`);
544
+ }
306
545
  function resolveLastUrl(payload) {
307
546
  const candidates = [payload.url, payload.to, payload.href, payload.location];
308
547
  for (const candidate of candidates) {
@@ -312,13 +551,38 @@ function resolveLastUrl(payload) {
312
551
  }
313
552
  return undefined;
314
553
  }
315
- function mapEventRecord(row) {
554
+ function mapEventRecord(row, profile = 'legacy', options = {}) {
555
+ const payload = readJsonPayload(row.payload_json);
556
+ if (profile === 'compact') {
557
+ const compact = {
558
+ eventId: row.event_id,
559
+ sessionId: row.session_id,
560
+ timestamp: row.ts,
561
+ type: row.type,
562
+ summary: describeEvent(row.type, payload),
563
+ };
564
+ if (row.type === 'console') {
565
+ compact.level = typeof payload.level === 'string' ? payload.level : undefined;
566
+ compact.message = typeof payload.message === 'string' ? payload.message : undefined;
567
+ }
568
+ if (row.type === 'nav') {
569
+ compact.url = resolveLastUrl(payload);
570
+ }
571
+ if (options.includePayload === true) {
572
+ compact.payload = payload;
573
+ }
574
+ return compact;
575
+ }
316
576
  return {
317
577
  eventId: row.event_id,
318
578
  sessionId: row.session_id,
319
579
  timestamp: row.ts,
320
580
  type: row.type,
321
- payload: readJsonPayload(row.payload_json),
581
+ tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
582
+ origin: row.origin
583
+ ?? (typeof payload.origin === 'string' ? payload.origin : undefined)
584
+ ?? undefined,
585
+ payload,
322
586
  };
323
587
  }
324
588
  function classifyNetworkFailure(status, errorClass) {
@@ -376,6 +640,153 @@ function resolveDurationMs(value, fallback, maxValue) {
376
640
  }
377
641
  return Math.min(floored, maxValue);
378
642
  }
643
+ function resolveBodyChunkBytes(value) {
644
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
645
+ return DEFAULT_BODY_CHUNK_BYTES;
646
+ }
647
+ const floored = Math.floor(value);
648
+ if (floored < 1) {
649
+ return DEFAULT_BODY_CHUNK_BYTES;
650
+ }
651
+ return Math.min(floored, MAX_BODY_CHUNK_BYTES);
652
+ }
653
+ function resolveTimeoutMs(value, fallback, maxValue) {
654
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
655
+ return fallback;
656
+ }
657
+ const floored = Math.floor(value);
658
+ if (floored < 100) {
659
+ return fallback;
660
+ }
661
+ return Math.min(floored, maxValue);
662
+ }
663
+ function normalizeHttpMethod(value) {
664
+ if (typeof value !== 'string') {
665
+ return undefined;
666
+ }
667
+ const normalized = value.trim().toUpperCase();
668
+ return normalized.length > 0 ? normalized : undefined;
669
+ }
670
+ function normalizeOptionalString(value) {
671
+ if (typeof value !== 'string') {
672
+ return undefined;
673
+ }
674
+ const trimmed = value.trim();
675
+ return trimmed.length > 0 ? trimmed : undefined;
676
+ }
677
+ function normalizeStatusIn(value) {
678
+ if (!Array.isArray(value)) {
679
+ return [];
680
+ }
681
+ const statuses = value
682
+ .filter((entry) => typeof entry === 'number' && Number.isFinite(entry))
683
+ .map((entry) => Math.floor(entry))
684
+ .filter((entry) => entry >= 100 && entry <= 599);
685
+ return Array.from(new Set(statuses));
686
+ }
687
+ function parseJsonOrUndefined(value) {
688
+ if (!value) {
689
+ return undefined;
690
+ }
691
+ try {
692
+ return JSON.parse(value);
693
+ }
694
+ catch {
695
+ return undefined;
696
+ }
697
+ }
698
+ function compileSafeRegex(value) {
699
+ if (!value) {
700
+ return undefined;
701
+ }
702
+ try {
703
+ return new RegExp(value);
704
+ }
705
+ catch {
706
+ throw new Error('urlRegex must be a valid regular expression');
707
+ }
708
+ }
709
+ function mapNetworkCallRecord(row, includeBodies) {
710
+ const requestBodyJson = parseJsonOrUndefined(row.request_body_json);
711
+ const responseBodyJson = parseJsonOrUndefined(row.response_body_json);
712
+ return {
713
+ requestId: row.request_id,
714
+ sessionId: row.session_id,
715
+ traceId: row.trace_id ?? undefined,
716
+ tabId: row.tab_id ?? undefined,
717
+ timestamp: row.ts_start,
718
+ durationMs: row.duration_ms ?? undefined,
719
+ method: row.method,
720
+ url: row.url,
721
+ origin: row.origin ?? undefined,
722
+ status: row.status ?? undefined,
723
+ initiator: row.initiator ?? undefined,
724
+ errorType: classifyNetworkFailure(row.status, row.error_class),
725
+ responseSizeEst: row.response_size_est ?? undefined,
726
+ request: {
727
+ contentType: row.request_content_type ?? undefined,
728
+ bodyBytes: row.request_body_bytes ?? undefined,
729
+ truncated: row.request_body_truncated === 1,
730
+ bodyChunkRef: row.request_body_chunk_ref ?? undefined,
731
+ bodyJson: includeBodies ? requestBodyJson : undefined,
732
+ bodyText: includeBodies ? row.request_body_text ?? undefined : undefined,
733
+ },
734
+ response: {
735
+ contentType: row.response_content_type ?? undefined,
736
+ bodyBytes: row.response_body_bytes ?? undefined,
737
+ truncated: row.response_body_truncated === 1,
738
+ bodyChunkRef: row.response_body_chunk_ref ?? undefined,
739
+ bodyJson: includeBodies ? responseBodyJson : undefined,
740
+ bodyText: includeBodies ? row.response_body_text ?? undefined : undefined,
741
+ },
742
+ };
743
+ }
744
+ function mapBodyChunkRecord(row, offset, limit) {
745
+ const fullBuffer = Buffer.from(row.body_text, 'utf-8');
746
+ if (offset >= fullBuffer.byteLength) {
747
+ return {
748
+ chunkRef: row.chunk_ref,
749
+ sessionId: row.session_id,
750
+ requestId: row.request_id ?? undefined,
751
+ traceId: row.trace_id ?? undefined,
752
+ bodyKind: row.body_kind,
753
+ contentType: row.content_type ?? undefined,
754
+ totalBytes: fullBuffer.byteLength,
755
+ offset,
756
+ returnedBytes: 0,
757
+ hasMore: false,
758
+ nextOffset: null,
759
+ chunkText: '',
760
+ truncated: row.truncated === 1,
761
+ createdAt: row.created_at,
762
+ };
763
+ }
764
+ const chunkBuffer = fullBuffer.subarray(offset, Math.min(offset + limit, fullBuffer.byteLength));
765
+ const returnedBytes = chunkBuffer.byteLength;
766
+ const nextOffset = offset + returnedBytes;
767
+ const hasMore = nextOffset < fullBuffer.byteLength;
768
+ return {
769
+ chunkRef: row.chunk_ref,
770
+ sessionId: row.session_id,
771
+ requestId: row.request_id ?? undefined,
772
+ traceId: row.trace_id ?? undefined,
773
+ bodyKind: row.body_kind,
774
+ contentType: row.content_type ?? undefined,
775
+ totalBytes: fullBuffer.byteLength,
776
+ offset,
777
+ returnedBytes,
778
+ hasMore,
779
+ nextOffset: hasMore ? nextOffset : null,
780
+ chunkText: chunkBuffer.toString('utf-8'),
781
+ truncated: row.truncated === 1,
782
+ createdAt: row.created_at,
783
+ };
784
+ }
785
+ function sleep(ms) {
786
+ return new Promise((resolvePromise) => {
787
+ setTimeout(resolvePromise, ms);
788
+ });
789
+ }
379
790
  function normalizeAssetPath(pathValue) {
380
791
  return pathValue.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
381
792
  }
@@ -528,6 +939,59 @@ function asStringArray(value, maxItems) {
528
939
  .filter((entry) => typeof entry === 'string' && entry.length > 0)
529
940
  .slice(0, maxItems);
530
941
  }
942
+ const LIVE_CONSOLE_LEVELS = new Set(['log', 'info', 'warn', 'error', 'debug', 'trace']);
943
+ function resolveLiveConsoleLevels(value) {
944
+ const levels = asStringArray(value, 16)
945
+ .map((entry) => entry.trim().toLowerCase())
946
+ .filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
947
+ return Array.from(new Set(levels));
948
+ }
949
+ function asRecordArray(value) {
950
+ if (!Array.isArray(value)) {
951
+ return [];
952
+ }
953
+ return value.filter((entry) => typeof entry === 'object' && entry !== null);
954
+ }
955
+ function mapLiveConsoleLogRecord(log, profile, options = {}) {
956
+ if (profile === 'compact') {
957
+ const compact = {
958
+ timestamp: typeof log.timestamp === 'number'
959
+ ? log.timestamp
960
+ : typeof log.ts === 'number'
961
+ ? log.ts
962
+ : undefined,
963
+ level: typeof log.level === 'string' ? log.level : undefined,
964
+ message: typeof log.message === 'string' ? log.message : '',
965
+ };
966
+ if (typeof log.count === 'number') {
967
+ compact.count = log.count;
968
+ }
969
+ if (typeof log.firstTimestamp === 'number') {
970
+ compact.firstTimestamp = log.firstTimestamp;
971
+ }
972
+ if (typeof log.lastTimestamp === 'number') {
973
+ compact.lastTimestamp = log.lastTimestamp;
974
+ }
975
+ if (options.includeArgs === true && Array.isArray(log.args)) {
976
+ compact.args = log.args;
977
+ }
978
+ return compact;
979
+ }
980
+ return log;
981
+ }
982
+ function resolveOptionalTabId(value) {
983
+ if (value === undefined || value === null || value === '') {
984
+ return undefined;
985
+ }
986
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
987
+ throw new Error('tabId must be an integer');
988
+ }
989
+ const tabId = Math.floor(value);
990
+ if (!Number.isInteger(tabId) || tabId < 0) {
991
+ throw new Error('tabId must be an integer');
992
+ }
993
+ return tabId;
994
+ }
531
995
  function isLiveSessionDisconnectedMessage(message) {
532
996
  const normalized = message.toLowerCase();
533
997
  return normalized.includes('no active extension connection')
@@ -570,6 +1034,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
570
1034
  const sinceMinutes = typeof input.sinceMinutes === 'number' ? input.sinceMinutes : undefined;
571
1035
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
572
1036
  const offset = resolveOffset(input.offset);
1037
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
573
1038
  const where = [];
574
1039
  const params = [];
575
1040
  if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
@@ -581,6 +1046,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
581
1046
  SELECT
582
1047
  session_id,
583
1048
  created_at,
1049
+ paused_at,
584
1050
  ended_at,
585
1051
  tab_id,
586
1052
  window_id,
@@ -598,11 +1064,13 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
598
1064
  LIMIT ? OFFSET ?
599
1065
  `;
600
1066
  const rows = db.prepare(sql).all(...params, limit + 1, offset);
601
- const truncated = rows.length > limit;
1067
+ const truncatedByLimit = rows.length > limit;
602
1068
  const sessions = rows.slice(0, limit).map((row) => ({
603
1069
  sessionId: row.session_id,
604
1070
  createdAt: row.created_at,
1071
+ pausedAt: row.paused_at ?? undefined,
605
1072
  endedAt: row.ended_at ?? undefined,
1073
+ status: row.ended_at ? 'ended' : row.paused_at ? 'paused' : 'active',
606
1074
  tabId: row.tab_id ?? undefined,
607
1075
  windowId: row.window_id ?? undefined,
608
1076
  urlStart: row.url_start ?? undefined,
@@ -635,17 +1103,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
635
1103
  };
636
1104
  })(),
637
1105
  }));
1106
+ const bytePage = applyByteBudget(sessions, maxResponseBytes);
1107
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
638
1108
  return {
639
1109
  ...createBaseResponse(),
640
1110
  limitsApplied: {
641
1111
  maxResults: limit,
642
1112
  truncated,
643
1113
  },
644
- pagination: {
645
- offset,
646
- returned: sessions.length,
647
- },
648
- sessions,
1114
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1115
+ responseBytes: bytePage.responseBytes,
1116
+ sessions: bytePage.items,
649
1117
  };
650
1118
  },
651
1119
  get_session_summary: async (input) => {
@@ -713,14 +1181,21 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
713
1181
  get_recent_events: async (input) => {
714
1182
  const db = getDb();
715
1183
  const sessionId = getSessionId(input);
716
- if (!sessionId) {
717
- throw new Error('sessionId is required');
718
- }
1184
+ const origin = normalizeRequestedOrigin(input.url);
1185
+ ensureSessionOrOriginFilter(sessionId, origin);
719
1186
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
720
1187
  const offset = resolveOffset(input.offset);
1188
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1189
+ const responseProfile = resolveResponseProfile(input.responseProfile);
1190
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
721
1191
  const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
722
- const params = [sessionId];
723
- const where = ['session_id = ?'];
1192
+ const params = [];
1193
+ const where = [];
1194
+ if (sessionId) {
1195
+ where.push('session_id = ?');
1196
+ params.push(sessionId);
1197
+ }
1198
+ appendEventOriginFilter(where, params, origin);
724
1199
  if (requestedTypes.length > 0) {
725
1200
  const placeholders = requestedTypes.map(() => '?').join(', ');
726
1201
  where.push(`type IN (${placeholders})`);
@@ -728,94 +1203,270 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
728
1203
  }
729
1204
  const rows = db
730
1205
  .prepare(`
731
- SELECT event_id, session_id, ts, type, payload_json
1206
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
732
1207
  FROM events
733
1208
  WHERE ${where.join(' AND ')}
734
1209
  ORDER BY ts DESC
735
1210
  LIMIT ? OFFSET ?
736
1211
  `)
737
1212
  .all(...params, limit + 1, offset);
738
- const truncated = rows.length > limit;
1213
+ const truncatedByLimit = rows.length > limit;
1214
+ const events = rows
1215
+ .slice(0, limit)
1216
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
1217
+ const bytePage = applyByteBudget(events, maxResponseBytes);
1218
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
739
1219
  return {
740
1220
  ...createBaseResponse(sessionId),
741
1221
  limitsApplied: {
742
1222
  maxResults: limit,
743
1223
  truncated,
744
1224
  },
745
- pagination: {
746
- offset,
747
- returned: Math.min(rows.length, limit),
748
- },
749
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
1225
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1226
+ responseProfile,
1227
+ responseBytes: bytePage.responseBytes,
1228
+ events: bytePage.items,
750
1229
  };
751
1230
  },
752
1231
  get_navigation_history: async (input) => {
753
1232
  const db = getDb();
754
1233
  const sessionId = getSessionId(input);
755
- if (!sessionId) {
756
- throw new Error('sessionId is required');
757
- }
1234
+ const origin = normalizeRequestedOrigin(input.url);
1235
+ ensureSessionOrOriginFilter(sessionId, origin);
758
1236
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
759
1237
  const offset = resolveOffset(input.offset);
1238
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1239
+ const responseProfile = resolveResponseProfile(input.responseProfile);
1240
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
1241
+ const params = [];
1242
+ const where = ["type = 'nav'"];
1243
+ if (sessionId) {
1244
+ where.push('session_id = ?');
1245
+ params.push(sessionId);
1246
+ }
1247
+ appendEventOriginFilter(where, params, origin);
760
1248
  const rows = db
761
1249
  .prepare(`
762
- SELECT event_id, session_id, ts, type, payload_json
1250
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
763
1251
  FROM events
764
- WHERE session_id = ? AND type = 'nav'
1252
+ WHERE ${where.join(' AND ')}
765
1253
  ORDER BY ts DESC
766
1254
  LIMIT ? OFFSET ?
767
1255
  `)
768
- .all(sessionId, limit + 1, offset);
769
- const truncated = rows.length > limit;
1256
+ .all(...params, limit + 1, offset);
1257
+ const truncatedByLimit = rows.length > limit;
1258
+ const events = rows
1259
+ .slice(0, limit)
1260
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
1261
+ const bytePage = applyByteBudget(events, maxResponseBytes);
1262
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
770
1263
  return {
771
1264
  ...createBaseResponse(sessionId),
772
1265
  limitsApplied: {
773
1266
  maxResults: limit,
774
1267
  truncated,
775
1268
  },
776
- pagination: {
777
- offset,
778
- returned: Math.min(rows.length, limit),
779
- },
780
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
1269
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1270
+ responseProfile,
1271
+ responseBytes: bytePage.responseBytes,
1272
+ events: bytePage.items,
781
1273
  };
782
1274
  },
783
1275
  get_console_events: async (input) => {
784
1276
  const db = getDb();
785
1277
  const sessionId = getSessionId(input);
786
- if (!sessionId) {
787
- throw new Error('sessionId is required');
788
- }
1278
+ const origin = normalizeRequestedOrigin(input.url);
1279
+ ensureSessionOrOriginFilter(sessionId, origin);
789
1280
  const level = typeof input.level === 'string' ? input.level : undefined;
790
1281
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
791
1282
  const offset = resolveOffset(input.offset);
792
- const params = [sessionId];
793
- let levelClause = '';
1283
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1284
+ const responseProfile = resolveResponseProfile(input.responseProfile);
1285
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
1286
+ const params = [];
1287
+ const where = ["type = 'console'"];
1288
+ if (sessionId) {
1289
+ where.push('session_id = ?');
1290
+ params.push(sessionId);
1291
+ }
1292
+ appendEventOriginFilter(where, params, origin);
794
1293
  if (level) {
795
- levelClause = "AND json_extract(payload_json, '$.level') = ?";
1294
+ where.push("json_extract(payload_json, '$.level') = ?");
796
1295
  params.push(level);
797
1296
  }
798
1297
  const rows = db
799
1298
  .prepare(`
800
- SELECT event_id, session_id, ts, type, payload_json
1299
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
801
1300
  FROM events
802
- WHERE session_id = ? AND type = 'console' ${levelClause}
1301
+ WHERE ${where.join(' AND ')}
803
1302
  ORDER BY ts DESC
804
1303
  LIMIT ? OFFSET ?
805
1304
  `)
806
1305
  .all(...params, limit + 1, offset);
807
- const truncated = rows.length > limit;
1306
+ const truncatedByLimit = rows.length > limit;
1307
+ const events = rows
1308
+ .slice(0, limit)
1309
+ .map((row) => mapEventRecord(row, responseProfile, { includePayload }));
1310
+ const bytePage = applyByteBudget(events, maxResponseBytes);
1311
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
808
1312
  return {
809
1313
  ...createBaseResponse(sessionId),
810
1314
  limitsApplied: {
811
1315
  maxResults: limit,
812
1316
  truncated,
813
1317
  },
814
- pagination: {
815
- offset,
816
- returned: Math.min(rows.length, limit),
1318
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1319
+ responseProfile,
1320
+ responseBytes: bytePage.responseBytes,
1321
+ events: bytePage.items,
1322
+ };
1323
+ },
1324
+ get_console_summary: async (input) => {
1325
+ const db = getDb();
1326
+ const sessionId = getSessionId(input);
1327
+ const origin = normalizeRequestedOrigin(input.url);
1328
+ ensureSessionOrOriginFilter(sessionId, origin);
1329
+ const level = typeof input.level === 'string' && input.level.length > 0 ? input.level : undefined;
1330
+ const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
1331
+ ? Math.floor(input.sinceMinutes)
1332
+ : undefined;
1333
+ const limit = resolveLimit(input.limit, 10);
1334
+ const where = ["type = 'console'"];
1335
+ const params = [];
1336
+ if (sessionId) {
1337
+ where.push('session_id = ?');
1338
+ params.push(sessionId);
1339
+ }
1340
+ appendEventOriginFilter(where, params, origin);
1341
+ if (level) {
1342
+ where.push("json_extract(payload_json, '$.level') = ?");
1343
+ params.push(level);
1344
+ }
1345
+ if (sinceMinutes !== undefined && sinceMinutes > 0) {
1346
+ where.push('ts >= ?');
1347
+ params.push(Date.now() - sinceMinutes * 60_000);
1348
+ }
1349
+ const whereClause = `WHERE ${where.join(' AND ')}`;
1350
+ const totals = db
1351
+ .prepare(`
1352
+ SELECT
1353
+ COUNT(*) AS total,
1354
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'log' THEN 1 ELSE 0 END) AS log_count,
1355
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'info' THEN 1 ELSE 0 END) AS info_count,
1356
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'warn' THEN 1 ELSE 0 END) AS warn_count,
1357
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'error' THEN 1 ELSE 0 END) AS error_count,
1358
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'debug' THEN 1 ELSE 0 END) AS debug_count,
1359
+ SUM(CASE WHEN json_extract(payload_json, '$.level') = 'trace' THEN 1 ELSE 0 END) AS trace_count,
1360
+ MIN(ts) AS first_ts,
1361
+ MAX(ts) AS last_ts
1362
+ FROM events
1363
+ ${whereClause}
1364
+ `)
1365
+ .get(...params);
1366
+ const topMessages = db
1367
+ .prepare(`
1368
+ SELECT
1369
+ COALESCE(json_extract(payload_json, '$.message'), 'console event') AS message,
1370
+ COALESCE(json_extract(payload_json, '$.level'), 'log') AS level,
1371
+ COUNT(*) AS count,
1372
+ MIN(ts) AS first_ts,
1373
+ MAX(ts) AS last_ts
1374
+ FROM events
1375
+ ${whereClause}
1376
+ GROUP BY message, level
1377
+ ORDER BY count DESC, last_ts DESC
1378
+ LIMIT ?
1379
+ `)
1380
+ .all(...params, limit);
1381
+ return {
1382
+ ...createBaseResponse(sessionId),
1383
+ limitsApplied: {
1384
+ maxResults: limit,
1385
+ truncated: false,
1386
+ },
1387
+ counts: {
1388
+ total: totals.total ?? 0,
1389
+ byLevel: {
1390
+ log: totals.log_count ?? 0,
1391
+ info: totals.info_count ?? 0,
1392
+ warn: totals.warn_count ?? 0,
1393
+ error: totals.error_count ?? 0,
1394
+ debug: totals.debug_count ?? 0,
1395
+ trace: totals.trace_count ?? 0,
1396
+ },
817
1397
  },
818
- events: rows.slice(0, limit).map((row) => mapEventRecord(row)),
1398
+ firstSeenAt: totals.first_ts ?? undefined,
1399
+ lastSeenAt: totals.last_ts ?? undefined,
1400
+ topMessages: topMessages.map((entry) => ({
1401
+ level: entry.level,
1402
+ message: entry.message,
1403
+ count: entry.count,
1404
+ firstSeenAt: entry.first_ts,
1405
+ lastSeenAt: entry.last_ts,
1406
+ })),
1407
+ };
1408
+ },
1409
+ get_event_summary: async (input) => {
1410
+ const db = getDb();
1411
+ const sessionId = getSessionId(input);
1412
+ const origin = normalizeRequestedOrigin(input.url);
1413
+ ensureSessionOrOriginFilter(sessionId, origin);
1414
+ const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
1415
+ const sinceMinutes = typeof input.sinceMinutes === 'number' && Number.isFinite(input.sinceMinutes)
1416
+ ? Math.floor(input.sinceMinutes)
1417
+ : undefined;
1418
+ const limit = resolveLimit(input.limit, 20);
1419
+ const where = [];
1420
+ const params = [];
1421
+ if (sessionId) {
1422
+ where.push('session_id = ?');
1423
+ params.push(sessionId);
1424
+ }
1425
+ appendEventOriginFilter(where, params, origin);
1426
+ if (requestedTypes.length > 0) {
1427
+ const placeholders = requestedTypes.map(() => '?').join(', ');
1428
+ where.push(`type IN (${placeholders})`);
1429
+ params.push(...requestedTypes);
1430
+ }
1431
+ if (sinceMinutes !== undefined && sinceMinutes > 0) {
1432
+ where.push('ts >= ?');
1433
+ params.push(Date.now() - sinceMinutes * 60_000);
1434
+ }
1435
+ const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
1436
+ const totals = db
1437
+ .prepare(`
1438
+ SELECT COUNT(*) AS total, MIN(ts) AS first_ts, MAX(ts) AS last_ts
1439
+ FROM events
1440
+ ${whereClause}
1441
+ `)
1442
+ .get(...params);
1443
+ const byType = db
1444
+ .prepare(`
1445
+ SELECT type, COUNT(*) AS count, MIN(ts) AS first_ts, MAX(ts) AS last_ts
1446
+ FROM events
1447
+ ${whereClause}
1448
+ GROUP BY type
1449
+ ORDER BY count DESC, last_ts DESC
1450
+ LIMIT ?
1451
+ `)
1452
+ .all(...params, limit);
1453
+ return {
1454
+ ...createBaseResponse(sessionId),
1455
+ limitsApplied: {
1456
+ maxResults: limit,
1457
+ truncated: false,
1458
+ },
1459
+ counts: {
1460
+ total: totals.total ?? 0,
1461
+ },
1462
+ firstSeenAt: totals.first_ts ?? undefined,
1463
+ lastSeenAt: totals.last_ts ?? undefined,
1464
+ byType: byType.map((entry) => ({
1465
+ type: entry.type,
1466
+ count: entry.count,
1467
+ firstSeenAt: entry.first_ts,
1468
+ lastSeenAt: entry.last_ts,
1469
+ })),
819
1470
  };
820
1471
  },
821
1472
  get_error_fingerprints: async (input) => {
@@ -826,6 +1477,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
826
1477
  : undefined;
827
1478
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
828
1479
  const offset = resolveOffset(input.offset);
1480
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
829
1481
  const params = [];
830
1482
  const where = [];
831
1483
  if (sessionId) {
@@ -846,35 +1498,39 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
846
1498
  LIMIT ? OFFSET ?
847
1499
  `)
848
1500
  .all(...params, limit + 1, offset);
849
- const truncated = rows.length > limit;
1501
+ const truncatedByLimit = rows.length > limit;
1502
+ const fingerprints = rows.slice(0, limit).map((row) => ({
1503
+ fingerprint: row.fingerprint,
1504
+ sessionId: row.session_id,
1505
+ count: row.count,
1506
+ sampleMessage: row.sample_message,
1507
+ sampleStack: row.sample_stack ?? undefined,
1508
+ firstSeenAt: row.first_seen_at,
1509
+ lastSeenAt: row.last_seen_at,
1510
+ }));
1511
+ const bytePage = applyByteBudget(fingerprints, maxResponseBytes);
1512
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
850
1513
  return {
851
1514
  ...createBaseResponse(sessionId),
852
1515
  limitsApplied: {
853
1516
  maxResults: limit,
854
1517
  truncated,
855
1518
  },
856
- pagination: {
857
- offset,
858
- returned: Math.min(rows.length, limit),
859
- },
860
- fingerprints: rows.slice(0, limit).map((row) => ({
861
- fingerprint: row.fingerprint,
862
- sessionId: row.session_id,
863
- count: row.count,
864
- sampleMessage: row.sample_message,
865
- sampleStack: row.sample_stack ?? undefined,
866
- firstSeenAt: row.first_seen_at,
867
- lastSeenAt: row.last_seen_at,
868
- })),
1519
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1520
+ responseBytes: bytePage.responseBytes,
1521
+ fingerprints: bytePage.items,
869
1522
  };
870
1523
  },
871
1524
  get_network_failures: async (input) => {
872
1525
  const db = getDb();
873
1526
  const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
1527
+ const origin = normalizeRequestedOrigin(input.url);
1528
+ ensureSessionOrOriginFilter(sessionId, origin);
874
1529
  const groupBy = typeof input.groupBy === 'string' ? input.groupBy : undefined;
875
1530
  const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
876
1531
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
877
1532
  const offset = resolveOffset(input.offset);
1533
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
878
1534
  const params = [];
879
1535
  const where = [];
880
1536
  const errorFilter = buildNetworkFailureFilter(errorType);
@@ -882,6 +1538,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
882
1538
  where.push('session_id = ?');
883
1539
  params.push(sessionId);
884
1540
  }
1541
+ appendNetworkOriginFilter(where, params, origin);
885
1542
  where.push(errorFilter);
886
1543
  if (errorFilter === 'error_class = ?' && errorType) {
887
1544
  params.push(errorType);
@@ -907,57 +1564,327 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
907
1564
  LIMIT ? OFFSET ?
908
1565
  `)
909
1566
  .all(...params, limit + 1, offset);
910
- const truncated = rows.length > limit;
1567
+ const truncatedByLimit = rows.length > limit;
1568
+ const groups = rows.slice(0, limit).map((row) => ({
1569
+ key: row.group_key,
1570
+ count: row.count,
1571
+ firstSeenAt: row.first_ts,
1572
+ lastSeenAt: row.last_ts,
1573
+ }));
1574
+ const bytePage = applyByteBudget(groups, maxResponseBytes);
1575
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
911
1576
  return {
912
1577
  ...createBaseResponse(sessionId),
913
1578
  limitsApplied: {
914
1579
  maxResults: limit,
915
1580
  truncated,
916
1581
  },
917
- pagination: {
918
- offset,
919
- returned: Math.min(rows.length, limit),
920
- },
1582
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1583
+ responseBytes: bytePage.responseBytes,
921
1584
  groupBy,
922
- groups: rows.slice(0, limit).map((row) => ({
923
- key: row.group_key,
924
- count: row.count,
925
- firstSeenAt: row.first_ts,
926
- lastSeenAt: row.last_ts,
927
- })),
1585
+ groups: bytePage.items,
928
1586
  };
929
1587
  }
930
1588
  const rows = db
931
1589
  .prepare(`
932
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1590
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
933
1591
  FROM network
934
1592
  ${whereClause}
935
1593
  ORDER BY ts_start DESC
936
1594
  LIMIT ? OFFSET ?
937
1595
  `)
938
1596
  .all(...params, limit + 1, offset);
939
- const truncated = rows.length > limit;
1597
+ const truncatedByLimit = rows.length > limit;
1598
+ const failures = rows.slice(0, limit).map((row) => ({
1599
+ requestId: row.request_id,
1600
+ sessionId: row.session_id,
1601
+ traceId: row.trace_id ?? undefined,
1602
+ tabId: row.tab_id ?? undefined,
1603
+ timestamp: row.ts_start,
1604
+ durationMs: row.duration_ms ?? undefined,
1605
+ method: row.method,
1606
+ url: row.url,
1607
+ origin: row.origin ?? undefined,
1608
+ status: row.status ?? undefined,
1609
+ initiator: row.initiator ?? undefined,
1610
+ errorType: classifyNetworkFailure(row.status, row.error_class),
1611
+ }));
1612
+ const bytePage = applyByteBudget(failures, maxResponseBytes);
1613
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
940
1614
  return {
941
1615
  ...createBaseResponse(sessionId),
942
1616
  limitsApplied: {
943
1617
  maxResults: limit,
944
1618
  truncated,
945
1619
  },
946
- pagination: {
947
- offset,
948
- returned: Math.min(rows.length, limit),
1620
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1621
+ responseBytes: bytePage.responseBytes,
1622
+ failures: bytePage.items,
1623
+ };
1624
+ },
1625
+ get_network_calls: async (input) => {
1626
+ const db = getDb();
1627
+ const sessionId = getSessionId(input);
1628
+ if (!sessionId) {
1629
+ throw new Error('sessionId is required');
1630
+ }
1631
+ const includeBodies = input.includeBodies === true;
1632
+ const urlContains = normalizeOptionalString(input.urlContains);
1633
+ const urlRegex = compileSafeRegex(normalizeOptionalString(input.urlRegex));
1634
+ const method = normalizeHttpMethod(input.method);
1635
+ const statusIn = normalizeStatusIn(input.statusIn);
1636
+ const tabId = resolveOptionalTabId(input.tabId);
1637
+ const timeFrom = resolveOptionalTimestamp(input.timeFrom);
1638
+ const timeTo = resolveOptionalTimestamp(input.timeTo);
1639
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
1640
+ const offset = resolveOffset(input.offset);
1641
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1642
+ if (timeFrom !== undefined && timeTo !== undefined && timeFrom > timeTo) {
1643
+ throw new Error('timeFrom must be <= timeTo');
1644
+ }
1645
+ const where = ['session_id = ?'];
1646
+ const params = [sessionId];
1647
+ if (urlContains) {
1648
+ where.push('url LIKE ?');
1649
+ params.push(`%${urlContains}%`);
1650
+ }
1651
+ if (method) {
1652
+ where.push('method = ?');
1653
+ params.push(method);
1654
+ }
1655
+ if (statusIn.length > 0) {
1656
+ where.push(`status IN (${statusIn.map(() => '?').join(', ')})`);
1657
+ params.push(...statusIn);
1658
+ }
1659
+ if (tabId !== undefined) {
1660
+ where.push('tab_id = ?');
1661
+ params.push(tabId);
1662
+ }
1663
+ if (timeFrom !== undefined) {
1664
+ where.push('ts_start >= ?');
1665
+ params.push(timeFrom);
1666
+ }
1667
+ if (timeTo !== undefined) {
1668
+ where.push('ts_start <= ?');
1669
+ params.push(timeTo);
1670
+ }
1671
+ const whereClause = `WHERE ${where.join(' AND ')}`;
1672
+ if (!urlRegex) {
1673
+ const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
1674
+ FROM network
1675
+ ${whereClause}
1676
+ ORDER BY ts_start DESC
1677
+ LIMIT ? OFFSET ?`).all(...params, limit + 1, offset);
1678
+ const truncatedByLimit = rows.length > limit;
1679
+ const calls = rows
1680
+ .slice(0, limit)
1681
+ .map((row) => mapNetworkCallRecord(row, includeBodies));
1682
+ const bytePage = applyByteBudget(calls, maxResponseBytes);
1683
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1684
+ return {
1685
+ ...createBaseResponse(sessionId),
1686
+ limitsApplied: {
1687
+ maxResults: limit,
1688
+ truncated,
1689
+ },
1690
+ filtersApplied: {
1691
+ sessionId,
1692
+ urlContains,
1693
+ method,
1694
+ statusIn,
1695
+ tabId,
1696
+ timeFrom,
1697
+ timeTo,
1698
+ includeBodies,
1699
+ },
1700
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1701
+ responseBytes: bytePage.responseBytes,
1702
+ calls: bytePage.items,
1703
+ };
1704
+ }
1705
+ const regexScanLimit = Math.min(Math.max(limit + offset + 200, 500), 5000);
1706
+ const regex = urlRegex;
1707
+ const regexRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
1708
+ FROM network
1709
+ ${whereClause}
1710
+ ORDER BY ts_start DESC
1711
+ LIMIT ?`).all(...params, regexScanLimit);
1712
+ const matched = regexRows.filter((row) => regex.test(row.url));
1713
+ const sliced = matched.slice(offset, offset + limit + 1);
1714
+ const truncatedByLimit = matched.length > offset + limit;
1715
+ const calls = sliced
1716
+ .slice(0, limit)
1717
+ .map((row) => mapNetworkCallRecord(row, includeBodies));
1718
+ const bytePage = applyByteBudget(calls, maxResponseBytes);
1719
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1720
+ return {
1721
+ ...createBaseResponse(sessionId),
1722
+ limitsApplied: {
1723
+ maxResults: limit,
1724
+ truncated,
949
1725
  },
950
- failures: rows.slice(0, limit).map((row) => ({
951
- requestId: row.request_id,
952
- sessionId: row.session_id,
953
- timestamp: row.ts_start,
954
- durationMs: row.duration_ms ?? undefined,
955
- method: row.method,
956
- url: row.url,
957
- status: row.status ?? undefined,
958
- initiator: row.initiator ?? undefined,
959
- errorType: classifyNetworkFailure(row.status, row.error_class),
960
- })),
1726
+ filtersApplied: {
1727
+ sessionId,
1728
+ urlContains,
1729
+ urlRegex: urlRegex.source,
1730
+ method,
1731
+ statusIn,
1732
+ tabId,
1733
+ timeFrom,
1734
+ timeTo,
1735
+ includeBodies,
1736
+ regexScanLimit,
1737
+ },
1738
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1739
+ responseBytes: bytePage.responseBytes,
1740
+ calls: bytePage.items,
1741
+ };
1742
+ },
1743
+ wait_for_network_call: async (input) => {
1744
+ const db = getDb();
1745
+ const sessionId = getSessionId(input);
1746
+ if (!sessionId) {
1747
+ throw new Error('sessionId is required');
1748
+ }
1749
+ const urlPattern = normalizeOptionalString(input.urlPattern);
1750
+ if (!urlPattern) {
1751
+ throw new Error('urlPattern is required');
1752
+ }
1753
+ const method = normalizeHttpMethod(input.method);
1754
+ const timeoutMs = resolveTimeoutMs(input.timeoutMs, DEFAULT_NETWORK_POLL_TIMEOUT_MS, MAX_NETWORK_POLL_TIMEOUT_MS);
1755
+ const includeBodies = input.includeBodies === true;
1756
+ const startedAt = Date.now();
1757
+ const deadline = startedAt + timeoutMs;
1758
+ const urlRegex = compileSafeRegex(urlPattern);
1759
+ if (!urlRegex) {
1760
+ throw new Error('urlPattern is required');
1761
+ }
1762
+ while (Date.now() <= deadline) {
1763
+ const where = ['session_id = ?', 'ts_start >= ?'];
1764
+ const params = [sessionId, startedAt];
1765
+ if (method) {
1766
+ where.push('method = ?');
1767
+ params.push(method);
1768
+ }
1769
+ const rows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
1770
+ FROM network
1771
+ WHERE ${where.join(' AND ')}
1772
+ ORDER BY ts_start ASC
1773
+ LIMIT 200`).all(...params);
1774
+ const matched = rows.find((row) => urlRegex.test(row.url));
1775
+ if (matched) {
1776
+ return {
1777
+ ...createBaseResponse(sessionId),
1778
+ limitsApplied: {
1779
+ maxResults: 1,
1780
+ truncated: false,
1781
+ },
1782
+ waitedMs: Date.now() - startedAt,
1783
+ filter: {
1784
+ urlPattern,
1785
+ method,
1786
+ timeoutMs,
1787
+ includeBodies,
1788
+ },
1789
+ call: mapNetworkCallRecord(matched, includeBodies),
1790
+ };
1791
+ }
1792
+ await sleep(DEFAULT_NETWORK_POLL_INTERVAL_MS);
1793
+ }
1794
+ throw new Error(`No matching network call for pattern "${urlPattern}" within ${timeoutMs}ms.`);
1795
+ },
1796
+ get_request_trace: async (input) => {
1797
+ const db = getDb();
1798
+ const sessionId = getSessionId(input);
1799
+ const includeBodies = input.includeBodies === true;
1800
+ const requestId = normalizeOptionalString(input.requestId);
1801
+ const traceIdInput = normalizeOptionalString(input.traceId);
1802
+ const eventLimit = resolveLimit(input.eventLimit, DEFAULT_EVENT_LIMIT);
1803
+ if (!requestId && !traceIdInput) {
1804
+ throw new Error('requestId or traceId is required');
1805
+ }
1806
+ let anchor;
1807
+ if (requestId) {
1808
+ const params = [requestId];
1809
+ let sql = `SELECT ${NETWORK_CALL_SELECT_COLUMNS} FROM network WHERE request_id = ?`;
1810
+ if (sessionId) {
1811
+ sql += ' AND session_id = ?';
1812
+ params.push(sessionId);
1813
+ }
1814
+ sql += ' LIMIT 1';
1815
+ anchor = db.prepare(sql).get(...params);
1816
+ if (!anchor) {
1817
+ throw new Error(`Request not found: ${requestId}`);
1818
+ }
1819
+ }
1820
+ const traceId = traceIdInput ?? anchor?.trace_id ?? null;
1821
+ const traceSessionId = sessionId ?? anchor?.session_id;
1822
+ const networkWhere = [];
1823
+ const networkParams = [];
1824
+ if (traceId) {
1825
+ networkWhere.push('trace_id = ?');
1826
+ networkParams.push(traceId);
1827
+ }
1828
+ else if (requestId) {
1829
+ networkWhere.push('request_id = ?');
1830
+ networkParams.push(requestId);
1831
+ }
1832
+ if (traceSessionId) {
1833
+ networkWhere.push('session_id = ?');
1834
+ networkParams.push(traceSessionId);
1835
+ }
1836
+ const networkRows = db.prepare(`SELECT ${NETWORK_CALL_SELECT_COLUMNS}
1837
+ FROM network
1838
+ WHERE ${networkWhere.join(' AND ')}
1839
+ ORDER BY ts_start ASC
1840
+ LIMIT 500`).all(...networkParams);
1841
+ const eventRows = traceId
1842
+ ? db.prepare(`SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1843
+ FROM events
1844
+ WHERE json_extract(payload_json, '$.traceId') = ?
1845
+ ${traceSessionId ? 'AND session_id = ?' : ''}
1846
+ ORDER BY ts ASC
1847
+ LIMIT ?`).all(...(traceSessionId ? [traceId, traceSessionId, eventLimit + 1] : [traceId, eventLimit + 1]))
1848
+ : [];
1849
+ const eventsTruncated = eventRows.length > eventLimit;
1850
+ const correlatedEvents = eventRows.slice(0, eventLimit).map((row) => mapEventRecord(row));
1851
+ return {
1852
+ ...createBaseResponse(traceSessionId),
1853
+ limitsApplied: {
1854
+ maxResults: eventLimit,
1855
+ truncated: eventsTruncated,
1856
+ },
1857
+ traceId: traceId ?? undefined,
1858
+ requestId: requestId ?? anchor?.request_id ?? undefined,
1859
+ anchorRequest: anchor ? mapNetworkCallRecord(anchor, includeBodies) : undefined,
1860
+ networkCalls: networkRows.map((row) => mapNetworkCallRecord(row, includeBodies)),
1861
+ correlatedEvents,
1862
+ };
1863
+ },
1864
+ get_body_chunk: async (input) => {
1865
+ const db = getDb();
1866
+ const chunkRef = normalizeOptionalString(input.chunkRef);
1867
+ if (!chunkRef) {
1868
+ throw new Error('chunkRef is required');
1869
+ }
1870
+ const sessionId = getSessionId(input);
1871
+ const offset = resolveOffset(input.offset);
1872
+ const limit = resolveBodyChunkBytes(input.limit);
1873
+ const row = db.prepare(`SELECT chunk_ref, session_id, request_id, trace_id, body_kind, content_type, body_text, body_bytes, truncated, created_at
1874
+ FROM body_chunks
1875
+ WHERE chunk_ref = ?
1876
+ ${sessionId ? 'AND session_id = ?' : ''}
1877
+ LIMIT 1`).get(...(sessionId ? [chunkRef, sessionId] : [chunkRef]));
1878
+ if (!row) {
1879
+ throw new Error(`Body chunk not found: ${chunkRef}`);
1880
+ }
1881
+ return {
1882
+ ...createBaseResponse(row.session_id),
1883
+ limitsApplied: {
1884
+ maxResults: limit,
1885
+ truncated: offset + limit < row.body_bytes,
1886
+ },
1887
+ ...mapBodyChunkRecord(row, offset, limit),
961
1888
  };
962
1889
  },
963
1890
  get_element_refs: async (input) => {
@@ -972,9 +1899,10 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
972
1899
  }
973
1900
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
974
1901
  const offset = resolveOffset(input.offset);
1902
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
975
1903
  const rows = db
976
1904
  .prepare(`
977
- SELECT event_id, session_id, ts, type, payload_json
1905
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
978
1906
  FROM events
979
1907
  WHERE session_id = ?
980
1908
  AND type IN ('ui', 'element_ref')
@@ -983,19 +1911,20 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
983
1911
  LIMIT ? OFFSET ?
984
1912
  `)
985
1913
  .all(sessionId, selector, limit + 1, offset);
986
- const truncated = rows.length > limit;
1914
+ const truncatedByLimit = rows.length > limit;
1915
+ const refs = rows.slice(0, limit).map((row) => mapEventRecord(row));
1916
+ const bytePage = applyByteBudget(refs, maxResponseBytes);
1917
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
987
1918
  return {
988
1919
  ...createBaseResponse(sessionId),
989
1920
  limitsApplied: {
990
1921
  maxResults: limit,
991
1922
  truncated,
992
1923
  },
993
- pagination: {
994
- offset,
995
- returned: Math.min(rows.length, limit),
996
- },
1924
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1925
+ responseBytes: bytePage.responseBytes,
997
1926
  selector,
998
- refs: rows.slice(0, limit).map((row) => mapEventRecord(row)),
1927
+ refs: bytePage.items,
999
1928
  };
1000
1929
  },
1001
1930
  explain_last_failure: async (input) => {
@@ -1008,7 +1937,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1008
1937
  const windowMs = lookbackSeconds * 1000;
1009
1938
  const latestErrorEvent = db
1010
1939
  .prepare(`
1011
- SELECT event_id, session_id, ts, type, payload_json
1940
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1012
1941
  FROM events
1013
1942
  WHERE session_id = ?
1014
1943
  AND (type = 'error' OR (type = 'console' AND json_extract(payload_json, '$.level') = 'error'))
@@ -1018,7 +1947,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1018
1947
  .get(sessionId);
1019
1948
  const latestNetworkFailure = db
1020
1949
  .prepare(`
1021
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1950
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1022
1951
  FROM network
1023
1952
  WHERE session_id = ?
1024
1953
  AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
@@ -1046,7 +1975,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1046
1975
  const windowEnd = anchorTs + 1_000;
1047
1976
  const eventRows = db
1048
1977
  .prepare(`
1049
- SELECT event_id, session_id, ts, type, payload_json
1978
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1050
1979
  FROM events
1051
1980
  WHERE session_id = ?
1052
1981
  AND ts BETWEEN ? AND ?
@@ -1055,7 +1984,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1055
1984
  .all(sessionId, windowStart, windowEnd);
1056
1985
  const networkRows = db
1057
1986
  .prepare(`
1058
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
1987
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1059
1988
  FROM network
1060
1989
  WHERE session_id = ?
1061
1990
  AND ts_start BETWEEN ? AND ?
@@ -1138,7 +2067,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1138
2067
  }
1139
2068
  const anchorEvent = db
1140
2069
  .prepare(`
1141
- SELECT event_id, session_id, ts, type, payload_json
2070
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1142
2071
  FROM events
1143
2072
  WHERE session_id = ? AND event_id = ?
1144
2073
  LIMIT 1
@@ -1153,7 +2082,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1153
2082
  const windowEnd = anchorEvent.ts + windowMs;
1154
2083
  const nearbyEvents = db
1155
2084
  .prepare(`
1156
- SELECT event_id, session_id, ts, type, payload_json
2085
+ SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
1157
2086
  FROM events
1158
2087
  WHERE session_id = ?
1159
2088
  AND event_id != ?
@@ -1162,7 +2091,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1162
2091
  .all(sessionId, eventId, windowStart, windowEnd);
1163
2092
  const nearbyNetworkFailures = db
1164
2093
  .prepare(`
1165
- SELECT request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class
2094
+ SELECT request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class
1166
2095
  FROM network
1167
2096
  WHERE session_id = ?
1168
2097
  AND ts_start BETWEEN ? AND ?
@@ -1234,6 +2163,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1234
2163
  const untilTimestamp = resolveOptionalTimestamp(input.untilTimestamp);
1235
2164
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
1236
2165
  const offset = resolveOffset(input.offset);
2166
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1237
2167
  const where = ['session_id = ?'];
1238
2168
  const params = [sessionId];
1239
2169
  if (trigger) {
@@ -1258,18 +2188,19 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1258
2188
  ORDER BY ts DESC
1259
2189
  LIMIT ? OFFSET ?`)
1260
2190
  .all(...params, limit + 1, offset);
1261
- const truncated = rows.length > limit;
2191
+ const truncatedByLimit = rows.length > limit;
2192
+ const snapshots = rows.slice(0, limit).map((row) => mapSnapshotMetadata(row));
2193
+ const bytePage = applyByteBudget(snapshots, maxResponseBytes);
2194
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
1262
2195
  return {
1263
2196
  ...createBaseResponse(sessionId),
1264
2197
  limitsApplied: {
1265
2198
  maxResults: limit,
1266
2199
  truncated,
1267
2200
  },
1268
- pagination: {
1269
- offset,
1270
- returned: Math.min(rows.length, limit),
1271
- },
1272
- snapshots: rows.slice(0, limit).map((row) => mapSnapshotMetadata(row)),
2201
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
2202
+ responseBytes: bytePage.responseBytes,
2203
+ snapshots: bytePage.items,
1273
2204
  };
1274
2205
  },
1275
2206
  get_snapshot_for_event: async (input) => {
@@ -1351,7 +2282,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1351
2282
  throw new Error('snapshotId is required');
1352
2283
  }
1353
2284
  const assetType = input.asset === 'png' ? 'png' : 'png';
1354
- const encoding = input.encoding === 'base64' ? 'base64' : 'raw';
2285
+ const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
1355
2286
  const offset = resolveOffset(input.offset);
1356
2287
  const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
1357
2288
  const snapshot = db
@@ -1381,6 +2312,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1381
2312
  },
1382
2313
  snapshotId,
1383
2314
  asset: assetType,
2315
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1384
2316
  mime: snapshot.png_mime ?? 'image/png',
1385
2317
  totalBytes: fullBuffer.byteLength,
1386
2318
  offset,
@@ -1404,6 +2336,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1404
2336
  },
1405
2337
  snapshotId,
1406
2338
  asset: assetType,
2339
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1407
2340
  mime: snapshot.png_mime ?? 'image/png',
1408
2341
  totalBytes: fullBuffer.byteLength,
1409
2342
  offset,
@@ -1531,6 +2464,9 @@ export function createV2ToolHandlers(captureClient) {
1531
2464
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1532
2465
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1533
2466
  const maxAncestors = resolveCaptureAncestors(input.maxAncestors, 4);
2467
+ const includeDom = typeof input.includeDom === 'boolean' ? input.includeDom : mode !== 'png';
2468
+ const includeStyles = typeof input.includeStyles === 'boolean' ? input.includeStyles : mode !== 'png';
2469
+ const includePngDataUrl = typeof input.includePngDataUrl === 'boolean' ? input.includePngDataUrl : mode !== 'png';
1534
2470
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
1535
2471
  selector,
1536
2472
  trigger,
@@ -1540,15 +2476,105 @@ export function createV2ToolHandlers(captureClient) {
1540
2476
  maxDepth,
1541
2477
  maxBytes,
1542
2478
  maxAncestors,
2479
+ includeDom,
2480
+ includeStyles,
2481
+ includePngDataUrl,
1543
2482
  llmRequested: true,
1544
2483
  }, 5_000);
2484
+ const payload = ensureCaptureSuccess(capture, sessionId);
2485
+ const snapshotRecord = structuredClone(payload);
2486
+ const snapshotRoot = snapshotRecord.snapshot;
2487
+ if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
2488
+ const snapshotObject = snapshotRoot;
2489
+ if (!includeDom) {
2490
+ delete snapshotObject.dom;
2491
+ }
2492
+ if (!includeStyles) {
2493
+ delete snapshotObject.styles;
2494
+ }
2495
+ }
2496
+ const png = snapshotRecord.png;
2497
+ if (!includePngDataUrl && typeof png === 'object' && png !== null) {
2498
+ delete png.dataUrl;
2499
+ }
1545
2500
  return {
1546
2501
  ...createBaseResponse(sessionId),
1547
2502
  limitsApplied: {
1548
2503
  maxResults: maxBytes,
1549
2504
  truncated: capture.truncated ?? false,
1550
2505
  },
1551
- ...ensureCaptureSuccess(capture, sessionId),
2506
+ includeDom,
2507
+ includeStyles,
2508
+ includePngDataUrl,
2509
+ ...snapshotRecord,
2510
+ };
2511
+ },
2512
+ get_live_console_logs: async (input) => {
2513
+ const sessionId = getSessionId(input);
2514
+ if (!sessionId) {
2515
+ throw new Error('sessionId is required');
2516
+ }
2517
+ const origin = normalizeRequestedOrigin(input.url);
2518
+ const tabId = resolveOptionalTabId(input.tabId);
2519
+ const levels = resolveLiveConsoleLevels(input.levels);
2520
+ const contains = typeof input.contains === 'string' && input.contains.trim().length > 0
2521
+ ? input.contains.trim()
2522
+ : undefined;
2523
+ const sinceTs = resolveOptionalTimestamp(input.sinceTs);
2524
+ const includeRuntimeErrors = input.includeRuntimeErrors !== false;
2525
+ const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
2526
+ const responseProfile = resolveResponseProfile(input.responseProfile);
2527
+ const includeArgs = responseProfile === 'compact' && input.includeArgs === true;
2528
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
2529
+ const dedupeWindowMs = resolveDurationMs(input.dedupeWindowMs, 0, 60_000);
2530
+ const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
2531
+ origin,
2532
+ tabId,
2533
+ levels,
2534
+ contains,
2535
+ sinceTs,
2536
+ includeRuntimeErrors,
2537
+ dedupeWindowMs,
2538
+ limit,
2539
+ }, 3_000);
2540
+ const payload = ensureCaptureSuccess(capture, sessionId);
2541
+ const rawLogs = asRecordArray(payload.logs);
2542
+ const logs = rawLogs.map((entry) => mapLiveConsoleLogRecord(entry, responseProfile, { includeArgs }));
2543
+ const bytePage = applyByteBudget(logs, maxResponseBytes);
2544
+ const truncated = (capture.truncated ?? false) || bytePage.truncatedByBytes;
2545
+ const paginationRecord = typeof payload.pagination === 'object' && payload.pagination !== null
2546
+ ? payload.pagination
2547
+ : {};
2548
+ const matched = typeof paginationRecord.matched === 'number'
2549
+ ? Math.max(0, Math.floor(paginationRecord.matched))
2550
+ : rawLogs.length;
2551
+ return {
2552
+ ...createBaseResponse(sessionId),
2553
+ limitsApplied: {
2554
+ maxResults: limit,
2555
+ truncated,
2556
+ },
2557
+ responseProfile,
2558
+ responseBytes: bytePage.responseBytes,
2559
+ logs: bytePage.items,
2560
+ pagination: {
2561
+ returned: bytePage.items.length,
2562
+ matched,
2563
+ hasMore: truncated,
2564
+ maxResponseBytes,
2565
+ },
2566
+ filtersApplied: typeof payload.filtersApplied === 'object' && payload.filtersApplied !== null
2567
+ ? payload.filtersApplied
2568
+ : {
2569
+ tabId,
2570
+ origin,
2571
+ levels,
2572
+ contains,
2573
+ sinceTs,
2574
+ includeRuntimeErrors,
2575
+ dedupeWindowMs,
2576
+ },
2577
+ bufferStats: payload.bufferStats,
1552
2578
  };
1553
2579
  },
1554
2580
  };
@@ -1578,6 +2604,15 @@ function createDefaultHandler(toolName) {
1578
2604
  };
1579
2605
  };
1580
2606
  }
2607
+ function attachResponseBytes(response) {
2608
+ if (typeof response.responseBytes === 'number' && Number.isFinite(response.responseBytes)) {
2609
+ return response;
2610
+ }
2611
+ return {
2612
+ ...response,
2613
+ responseBytes: estimateJsonBytes(response),
2614
+ };
2615
+ }
1581
2616
  export function createToolRegistry(overrides = {}) {
1582
2617
  return ALL_TOOLS.map((toolName) => {
1583
2618
  const schema = TOOL_SCHEMAS[toolName] ?? { type: 'object', properties: {} };
@@ -1594,7 +2629,8 @@ export async function routeToolCall(tools, toolName, input) {
1594
2629
  if (!tool) {
1595
2630
  throw new Error(`Unknown tool: ${toolName}`);
1596
2631
  }
1597
- return tool.handler(isRecord(input) ? input : {});
2632
+ const response = await tool.handler(isRecord(input) ? input : {});
2633
+ return attachResponseBytes(response);
1598
2634
  }
1599
2635
  export function createMCPServer(overrides = {}, options = {}) {
1600
2636
  const logger = options.logger ?? createDefaultMcpLogger();