browser-debug-mcp-bridge 1.6.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: {
@@ -44,6 +45,9 @@ const TOOL_SCHEMAS = {
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: {
@@ -53,6 +57,9 @@ const TOOL_SCHEMAS = {
53
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: {
@@ -63,6 +70,29 @@ const TOOL_SCHEMAS = {
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,6 +102,7 @@ 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: {
@@ -83,6 +114,56 @@ const TOOL_SCHEMAS = {
83
114
  groupBy: { type: 'string' },
84
115
  limit: { type: 'number' },
85
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' },
86
167
  },
87
168
  },
88
169
  get_element_refs: {
@@ -93,6 +174,7 @@ const TOOL_SCHEMAS = {
93
174
  selector: { type: 'string' },
94
175
  limit: { type: 'number' },
95
176
  offset: { type: 'number' },
177
+ maxResponseBytes: { type: 'number' },
96
178
  },
97
179
  },
98
180
  get_dom_subtree: {
@@ -142,6 +224,9 @@ const TOOL_SCHEMAS = {
142
224
  maxDepth: { type: 'number' },
143
225
  maxBytes: { type: 'number' },
144
226
  maxAncestors: { type: 'number' },
227
+ includeDom: { type: 'boolean' },
228
+ includeStyles: { type: 'boolean' },
229
+ includePngDataUrl: { type: 'boolean' },
145
230
  },
146
231
  },
147
232
  get_live_console_logs: {
@@ -155,7 +240,11 @@ const TOOL_SCHEMAS = {
155
240
  contains: { type: 'string' },
156
241
  sinceTs: { type: 'number' },
157
242
  includeRuntimeErrors: { type: 'boolean' },
243
+ dedupeWindowMs: { type: 'number' },
158
244
  limit: { type: 'number' },
245
+ responseProfile: { type: 'string' },
246
+ includeArgs: { type: 'boolean' },
247
+ maxResponseBytes: { type: 'number' },
159
248
  },
160
249
  },
161
250
  explain_last_failure: {
@@ -185,6 +274,7 @@ const TOOL_SCHEMAS = {
185
274
  untilTimestamp: { type: 'number' },
186
275
  limit: { type: 'number' },
187
276
  offset: { type: 'number' },
277
+ maxResponseBytes: { type: 'number' },
188
278
  },
189
279
  },
190
280
  get_snapshot_for_event: {
@@ -215,8 +305,14 @@ const TOOL_DESCRIPTIONS = {
215
305
  get_recent_events: 'Read recent events from a session',
216
306
  get_navigation_history: 'Read navigation events for a session',
217
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',
218
310
  get_error_fingerprints: 'List aggregated error fingerprints',
219
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',
220
316
  get_element_refs: 'Get element references by selector',
221
317
  get_dom_subtree: 'Capture a bounded DOM subtree',
222
318
  get_dom_document: 'Capture full document as outline or html',
@@ -239,9 +335,21 @@ const DEFAULT_REDACTION_SUMMARY = {
239
335
  const DEFAULT_LIST_LIMIT = 25;
240
336
  const DEFAULT_EVENT_LIMIT = 50;
241
337
  const MAX_LIMIT = 200;
338
+ const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
339
+ const MAX_RESPONSE_BYTES = 512 * 1024;
242
340
  const DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES = 64 * 1024;
243
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;
244
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
+ `;
245
353
  const NETWORK_DOMAIN_GROUP_SQL = `
246
354
  CASE
247
355
  WHEN instr(replace(replace(url, 'https://', ''), 'http://', ''), '/') > 0
@@ -280,6 +388,62 @@ function resolveOffset(value) {
280
388
  const floored = Math.floor(value);
281
389
  return floored < 0 ? 0 : floored;
282
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
+ }
283
447
  function readJsonPayload(payloadJson) {
284
448
  try {
285
449
  const parsed = JSON.parse(payloadJson);
@@ -387,8 +551,28 @@ function resolveLastUrl(payload) {
387
551
  }
388
552
  return undefined;
389
553
  }
390
- function mapEventRecord(row) {
554
+ function mapEventRecord(row, profile = 'legacy', options = {}) {
391
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
+ }
392
576
  return {
393
577
  eventId: row.event_id,
394
578
  sessionId: row.session_id,
@@ -456,6 +640,153 @@ function resolveDurationMs(value, fallback, maxValue) {
456
640
  }
457
641
  return Math.min(floored, maxValue);
458
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
+ }
459
790
  function normalizeAssetPath(pathValue) {
460
791
  return pathValue.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
461
792
  }
@@ -615,6 +946,39 @@ function resolveLiveConsoleLevels(value) {
615
946
  .filter((entry) => LIVE_CONSOLE_LEVELS.has(entry));
616
947
  return Array.from(new Set(levels));
617
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
+ }
618
982
  function resolveOptionalTabId(value) {
619
983
  if (value === undefined || value === null || value === '') {
620
984
  return undefined;
@@ -670,6 +1034,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
670
1034
  const sinceMinutes = typeof input.sinceMinutes === 'number' ? input.sinceMinutes : undefined;
671
1035
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
672
1036
  const offset = resolveOffset(input.offset);
1037
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
673
1038
  const where = [];
674
1039
  const params = [];
675
1040
  if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
@@ -681,6 +1046,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
681
1046
  SELECT
682
1047
  session_id,
683
1048
  created_at,
1049
+ paused_at,
684
1050
  ended_at,
685
1051
  tab_id,
686
1052
  window_id,
@@ -698,11 +1064,13 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
698
1064
  LIMIT ? OFFSET ?
699
1065
  `;
700
1066
  const rows = db.prepare(sql).all(...params, limit + 1, offset);
701
- const truncated = rows.length > limit;
1067
+ const truncatedByLimit = rows.length > limit;
702
1068
  const sessions = rows.slice(0, limit).map((row) => ({
703
1069
  sessionId: row.session_id,
704
1070
  createdAt: row.created_at,
1071
+ pausedAt: row.paused_at ?? undefined,
705
1072
  endedAt: row.ended_at ?? undefined,
1073
+ status: row.ended_at ? 'ended' : row.paused_at ? 'paused' : 'active',
706
1074
  tabId: row.tab_id ?? undefined,
707
1075
  windowId: row.window_id ?? undefined,
708
1076
  urlStart: row.url_start ?? undefined,
@@ -735,17 +1103,17 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
735
1103
  };
736
1104
  })(),
737
1105
  }));
1106
+ const bytePage = applyByteBudget(sessions, maxResponseBytes);
1107
+ const truncated = truncatedByLimit || bytePage.truncatedByBytes;
738
1108
  return {
739
1109
  ...createBaseResponse(),
740
1110
  limitsApplied: {
741
1111
  maxResults: limit,
742
1112
  truncated,
743
1113
  },
744
- pagination: {
745
- offset,
746
- returned: sessions.length,
747
- },
748
- sessions,
1114
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1115
+ responseBytes: bytePage.responseBytes,
1116
+ sessions: bytePage.items,
749
1117
  };
750
1118
  },
751
1119
  get_session_summary: async (input) => {
@@ -817,6 +1185,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
817
1185
  ensureSessionOrOriginFilter(sessionId, origin);
818
1186
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
819
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;
820
1191
  const requestedTypes = parseRequestedTypes(input.types ?? input.eventTypes);
821
1192
  const params = [];
822
1193
  const where = [];
@@ -839,18 +1210,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
839
1210
  LIMIT ? OFFSET ?
840
1211
  `)
841
1212
  .all(...params, limit + 1, offset);
842
- 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;
843
1219
  return {
844
1220
  ...createBaseResponse(sessionId),
845
1221
  limitsApplied: {
846
1222
  maxResults: limit,
847
1223
  truncated,
848
1224
  },
849
- pagination: {
850
- offset,
851
- returned: Math.min(rows.length, limit),
852
- },
853
- 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,
854
1229
  };
855
1230
  },
856
1231
  get_navigation_history: async (input) => {
@@ -860,6 +1235,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
860
1235
  ensureSessionOrOriginFilter(sessionId, origin);
861
1236
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
862
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;
863
1241
  const params = [];
864
1242
  const where = ["type = 'nav'"];
865
1243
  if (sessionId) {
@@ -876,18 +1254,22 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
876
1254
  LIMIT ? OFFSET ?
877
1255
  `)
878
1256
  .all(...params, limit + 1, offset);
879
- const truncated = rows.length > limit;
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;
880
1263
  return {
881
1264
  ...createBaseResponse(sessionId),
882
1265
  limitsApplied: {
883
1266
  maxResults: limit,
884
1267
  truncated,
885
1268
  },
886
- pagination: {
887
- offset,
888
- returned: Math.min(rows.length, limit),
889
- },
890
- 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,
891
1273
  };
892
1274
  },
893
1275
  get_console_events: async (input) => {
@@ -898,6 +1280,9 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
898
1280
  const level = typeof input.level === 'string' ? input.level : undefined;
899
1281
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
900
1282
  const offset = resolveOffset(input.offset);
1283
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1284
+ const responseProfile = resolveResponseProfile(input.responseProfile);
1285
+ const includePayload = responseProfile === 'compact' && input.includePayload === true;
901
1286
  const params = [];
902
1287
  const where = ["type = 'console'"];
903
1288
  if (sessionId) {
@@ -918,18 +1303,170 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
918
1303
  LIMIT ? OFFSET ?
919
1304
  `)
920
1305
  .all(...params, limit + 1, offset);
921
- 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;
922
1312
  return {
923
1313
  ...createBaseResponse(sessionId),
924
1314
  limitsApplied: {
925
1315
  maxResults: limit,
926
1316
  truncated,
927
1317
  },
928
- pagination: {
929
- offset,
930
- 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
+ },
931
1397
  },
932
- 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
+ })),
933
1470
  };
934
1471
  },
935
1472
  get_error_fingerprints: async (input) => {
@@ -940,6 +1477,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
940
1477
  : undefined;
941
1478
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
942
1479
  const offset = resolveOffset(input.offset);
1480
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
943
1481
  const params = [];
944
1482
  const where = [];
945
1483
  if (sessionId) {
@@ -960,26 +1498,27 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
960
1498
  LIMIT ? OFFSET ?
961
1499
  `)
962
1500
  .all(...params, limit + 1, offset);
963
- 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;
964
1513
  return {
965
1514
  ...createBaseResponse(sessionId),
966
1515
  limitsApplied: {
967
1516
  maxResults: limit,
968
1517
  truncated,
969
1518
  },
970
- pagination: {
971
- offset,
972
- returned: Math.min(rows.length, limit),
973
- },
974
- fingerprints: rows.slice(0, limit).map((row) => ({
975
- fingerprint: row.fingerprint,
976
- sessionId: row.session_id,
977
- count: row.count,
978
- sampleMessage: row.sample_message,
979
- sampleStack: row.sample_stack ?? undefined,
980
- firstSeenAt: row.first_seen_at,
981
- lastSeenAt: row.last_seen_at,
982
- })),
1519
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1520
+ responseBytes: bytePage.responseBytes,
1521
+ fingerprints: bytePage.items,
983
1522
  };
984
1523
  },
985
1524
  get_network_failures: async (input) => {
@@ -991,6 +1530,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
991
1530
  const errorType = typeof input.errorType === 'string' ? input.errorType : undefined;
992
1531
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
993
1532
  const offset = resolveOffset(input.offset);
1533
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
994
1534
  const params = [];
995
1535
  const where = [];
996
1536
  const errorFilter = buildNetworkFailureFilter(errorType);
@@ -1024,58 +1564,327 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1024
1564
  LIMIT ? OFFSET ?
1025
1565
  `)
1026
1566
  .all(...params, limit + 1, offset);
1027
- 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;
1028
1576
  return {
1029
1577
  ...createBaseResponse(sessionId),
1030
1578
  limitsApplied: {
1031
1579
  maxResults: limit,
1032
1580
  truncated,
1033
1581
  },
1034
- pagination: {
1035
- offset,
1036
- returned: Math.min(rows.length, limit),
1037
- },
1582
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1583
+ responseBytes: bytePage.responseBytes,
1038
1584
  groupBy,
1039
- groups: rows.slice(0, limit).map((row) => ({
1040
- key: row.group_key,
1041
- count: row.count,
1042
- firstSeenAt: row.first_ts,
1043
- lastSeenAt: row.last_ts,
1044
- })),
1585
+ groups: bytePage.items,
1045
1586
  };
1046
1587
  }
1047
1588
  const rows = db
1048
1589
  .prepare(`
1049
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
1050
1591
  FROM network
1051
1592
  ${whereClause}
1052
1593
  ORDER BY ts_start DESC
1053
1594
  LIMIT ? OFFSET ?
1054
1595
  `)
1055
1596
  .all(...params, limit + 1, offset);
1056
- 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;
1057
1614
  return {
1058
1615
  ...createBaseResponse(sessionId),
1059
1616
  limitsApplied: {
1060
1617
  maxResults: limit,
1061
1618
  truncated,
1062
1619
  },
1063
- pagination: {
1064
- offset,
1065
- 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,
1066
1725
  },
1067
- failures: rows.slice(0, limit).map((row) => ({
1068
- requestId: row.request_id,
1069
- sessionId: row.session_id,
1070
- timestamp: row.ts_start,
1071
- durationMs: row.duration_ms ?? undefined,
1072
- method: row.method,
1073
- url: row.url,
1074
- origin: row.origin ?? undefined,
1075
- status: row.status ?? undefined,
1076
- initiator: row.initiator ?? undefined,
1077
- errorType: classifyNetworkFailure(row.status, row.error_class),
1078
- })),
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),
1079
1888
  };
1080
1889
  },
1081
1890
  get_element_refs: async (input) => {
@@ -1090,6 +1899,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1090
1899
  }
1091
1900
  const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
1092
1901
  const offset = resolveOffset(input.offset);
1902
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1093
1903
  const rows = db
1094
1904
  .prepare(`
1095
1905
  SELECT event_id, session_id, ts, type, payload_json, tab_id, origin
@@ -1101,19 +1911,20 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1101
1911
  LIMIT ? OFFSET ?
1102
1912
  `)
1103
1913
  .all(sessionId, selector, limit + 1, offset);
1104
- 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;
1105
1918
  return {
1106
1919
  ...createBaseResponse(sessionId),
1107
1920
  limitsApplied: {
1108
1921
  maxResults: limit,
1109
1922
  truncated,
1110
1923
  },
1111
- pagination: {
1112
- offset,
1113
- returned: Math.min(rows.length, limit),
1114
- },
1924
+ pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
1925
+ responseBytes: bytePage.responseBytes,
1115
1926
  selector,
1116
- refs: rows.slice(0, limit).map((row) => mapEventRecord(row)),
1927
+ refs: bytePage.items,
1117
1928
  };
1118
1929
  },
1119
1930
  explain_last_failure: async (input) => {
@@ -1136,7 +1947,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1136
1947
  .get(sessionId);
1137
1948
  const latestNetworkFailure = db
1138
1949
  .prepare(`
1139
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
1140
1951
  FROM network
1141
1952
  WHERE session_id = ?
1142
1953
  AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
@@ -1173,7 +1984,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1173
1984
  .all(sessionId, windowStart, windowEnd);
1174
1985
  const networkRows = db
1175
1986
  .prepare(`
1176
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
1177
1988
  FROM network
1178
1989
  WHERE session_id = ?
1179
1990
  AND ts_start BETWEEN ? AND ?
@@ -1280,7 +2091,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1280
2091
  .all(sessionId, eventId, windowStart, windowEnd);
1281
2092
  const nearbyNetworkFailures = db
1282
2093
  .prepare(`
1283
- SELECT request_id, session_id, ts_start, duration_ms, method, url, origin, 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
1284
2095
  FROM network
1285
2096
  WHERE session_id = ?
1286
2097
  AND ts_start BETWEEN ? AND ?
@@ -1352,6 +2163,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1352
2163
  const untilTimestamp = resolveOptionalTimestamp(input.untilTimestamp);
1353
2164
  const limit = resolveLimit(input.limit, DEFAULT_LIST_LIMIT);
1354
2165
  const offset = resolveOffset(input.offset);
2166
+ const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
1355
2167
  const where = ['session_id = ?'];
1356
2168
  const params = [sessionId];
1357
2169
  if (trigger) {
@@ -1376,18 +2188,19 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1376
2188
  ORDER BY ts DESC
1377
2189
  LIMIT ? OFFSET ?`)
1378
2190
  .all(...params, limit + 1, offset);
1379
- 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;
1380
2195
  return {
1381
2196
  ...createBaseResponse(sessionId),
1382
2197
  limitsApplied: {
1383
2198
  maxResults: limit,
1384
2199
  truncated,
1385
2200
  },
1386
- pagination: {
1387
- offset,
1388
- returned: Math.min(rows.length, limit),
1389
- },
1390
- 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,
1391
2204
  };
1392
2205
  },
1393
2206
  get_snapshot_for_event: async (input) => {
@@ -1469,7 +2282,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1469
2282
  throw new Error('snapshotId is required');
1470
2283
  }
1471
2284
  const assetType = input.asset === 'png' ? 'png' : 'png';
1472
- const encoding = input.encoding === 'base64' ? 'base64' : 'raw';
2285
+ const encoding = input.encoding === 'raw' ? 'raw' : 'base64';
1473
2286
  const offset = resolveOffset(input.offset);
1474
2287
  const maxBytes = resolveChunkBytes(input.maxBytes, DEFAULT_SNAPSHOT_ASSET_CHUNK_BYTES);
1475
2288
  const snapshot = db
@@ -1499,6 +2312,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1499
2312
  },
1500
2313
  snapshotId,
1501
2314
  asset: assetType,
2315
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1502
2316
  mime: snapshot.png_mime ?? 'image/png',
1503
2317
  totalBytes: fullBuffer.byteLength,
1504
2318
  offset,
@@ -1522,6 +2336,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
1522
2336
  },
1523
2337
  snapshotId,
1524
2338
  asset: assetType,
2339
+ assetUri: `snapshot://${encodeURIComponent(sessionId)}/${encodeURIComponent(snapshotId)}/${assetType}`,
1525
2340
  mime: snapshot.png_mime ?? 'image/png',
1526
2341
  totalBytes: fullBuffer.byteLength,
1527
2342
  offset,
@@ -1649,6 +2464,9 @@ export function createV2ToolHandlers(captureClient) {
1649
2464
  const maxDepth = resolveCaptureDepth(input.maxDepth, 3);
1650
2465
  const maxBytes = resolveCaptureBytes(input.maxBytes, 50_000);
1651
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';
1652
2470
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_UI_SNAPSHOT', {
1653
2471
  selector,
1654
2472
  trigger,
@@ -1658,15 +2476,37 @@ export function createV2ToolHandlers(captureClient) {
1658
2476
  maxDepth,
1659
2477
  maxBytes,
1660
2478
  maxAncestors,
2479
+ includeDom,
2480
+ includeStyles,
2481
+ includePngDataUrl,
1661
2482
  llmRequested: true,
1662
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
+ }
1663
2500
  return {
1664
2501
  ...createBaseResponse(sessionId),
1665
2502
  limitsApplied: {
1666
2503
  maxResults: maxBytes,
1667
2504
  truncated: capture.truncated ?? false,
1668
2505
  },
1669
- ...ensureCaptureSuccess(capture, sessionId),
2506
+ includeDom,
2507
+ includeStyles,
2508
+ includePngDataUrl,
2509
+ ...snapshotRecord,
1670
2510
  };
1671
2511
  },
1672
2512
  get_live_console_logs: async (input) => {
@@ -1683,6 +2523,10 @@ export function createV2ToolHandlers(captureClient) {
1683
2523
  const sinceTs = resolveOptionalTimestamp(input.sinceTs);
1684
2524
  const includeRuntimeErrors = input.includeRuntimeErrors !== false;
1685
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);
1686
2530
  const capture = await executeLiveCapture(captureClient, sessionId, 'CAPTURE_GET_LIVE_CONSOLE_LOGS', {
1687
2531
  origin,
1688
2532
  tabId,
@@ -1690,15 +2534,47 @@ export function createV2ToolHandlers(captureClient) {
1690
2534
  contains,
1691
2535
  sinceTs,
1692
2536
  includeRuntimeErrors,
2537
+ dedupeWindowMs,
1693
2538
  limit,
1694
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;
1695
2551
  return {
1696
2552
  ...createBaseResponse(sessionId),
1697
2553
  limitsApplied: {
1698
2554
  maxResults: limit,
1699
- truncated: capture.truncated ?? false,
2555
+ truncated,
1700
2556
  },
1701
- ...ensureCaptureSuccess(capture, sessionId),
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,
1702
2578
  };
1703
2579
  },
1704
2580
  };
@@ -1728,6 +2604,15 @@ function createDefaultHandler(toolName) {
1728
2604
  };
1729
2605
  };
1730
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
+ }
1731
2616
  export function createToolRegistry(overrides = {}) {
1732
2617
  return ALL_TOOLS.map((toolName) => {
1733
2618
  const schema = TOOL_SCHEMAS[toolName] ?? { type: 'object', properties: {} };
@@ -1744,7 +2629,8 @@ export async function routeToolCall(tools, toolName, input) {
1744
2629
  if (!tool) {
1745
2630
  throw new Error(`Unknown tool: ${toolName}`);
1746
2631
  }
1747
- return tool.handler(isRecord(input) ? input : {});
2632
+ const response = await tool.handler(isRecord(input) ? input : {});
2633
+ return attachResponseBytes(response);
1748
2634
  }
1749
2635
  export function createMCPServer(overrides = {}, options = {}) {
1750
2636
  const logger = options.logger ?? createDefaultMcpLogger();