@debugg-ai/debugg-ai-mcp 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,16 +7,15 @@
7
7
  * (c) MCP-side aggregates per-target HAR slices into NetworkSummary[].
8
8
  *
9
9
  * The backend "Page Probe" workflow template runs:
10
- * browser.setup → loop[targets](page.navigate → page.capture) → done
10
+ * browser.setup → loop[targets](browser.navigate → browser.capture) → done
11
11
  *
12
- * Each page.capture node emits per-iteration outputData with consoleSlice
12
+ * Each browser.capture node emits per-iteration outputData with consoleSlice
13
13
  * + harSlice windowed to that URL's load span — that's what makes per-URL
14
14
  * networkSummary attribution accurate.
15
15
  */
16
16
  import { config } from '../config/index.js';
17
17
  import { Logger } from '../utils/logger.js';
18
18
  import { handleExternalServiceError } from '../utils/errors.js';
19
- import { imageContentBlock } from '../utils/imageUtils.js';
20
19
  import { DebuggAIServerClient } from '../services/index.js';
21
20
  import { TunnelProvisionError } from '../services/tunnels.js';
22
21
  import { tunnelManager } from '../services/ngrok/tunnelManager.js';
@@ -24,7 +23,7 @@ import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js
24
23
  import { extractLocalhostPort } from '../utils/urlParser.js';
25
24
  import { buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
26
25
  import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
27
- import { summarizeHar, summarizeConsole } from '../utils/harSummarizer.js';
26
+ import { reaggregateByOriginPath, mapConsoleSlice } from '../utils/harSummarizer.js';
28
27
  const logger = new Logger({ module: 'probePageHandler' });
29
28
  const TEMPLATE_KEYWORD = 'page probe';
30
29
  export async function probePageHandler(input, context, rawProgressCallback) {
@@ -155,13 +154,35 @@ export async function probePageHandler(input, context, rawProgressCallback) {
155
154
  `Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
156
155
  }
157
156
  // ── Build contextData (camelCase; axiosTransport snake_cases on the wire) ──
157
+ // Backend's browser.setup node (shared with App Evaluation + Raw Crawl
158
+ // templates) requires `target_url` (singular). The Page Probe template
159
+ // currently uses that node as-is — the per-target loop primitive is
160
+ // pending. Send BOTH:
161
+ // - targetUrl: first target's tunneled URL (satisfies browser.setup
162
+ // today; will keep working when the loop wraps it later)
163
+ // - targets[]: the full per-URL config for when the loop primitive
164
+ // ships and iterates over them
165
+ const firstTargetUrl = targetContexts[0]?.targetUrl ?? input.targets[0].url;
158
166
  const contextData = {
167
+ targetUrl: firstTargetUrl,
159
168
  targets: input.targets.map((t, i) => ({
160
169
  url: targetContexts[i].targetUrl ?? t.url,
161
- waitForSelector: t.waitForSelector,
170
+ // Send null (not undefined) for optional fields so the field exists
171
+ // in the target object even when the caller didn't pass one. Backend
172
+ // placeholder resolver was fixed in commit 154e1e69 to type-preserve
173
+ // null in single-placeholder substitutions, so null flows through.
174
+ waitForSelector: t.waitForSelector ?? null,
162
175
  waitForLoadState: t.waitForLoadState,
163
176
  timeoutMs: t.timeoutMs,
164
177
  })),
178
+ // Backend's browser.capture template binds {{include_dom}} and
179
+ // {{include_screenshot}} from contextData (verified 2026-04-29).
180
+ // The MCP-facing schema keeps `includeHtml` / `captureScreenshots`
181
+ // for caller ergonomics; we just map them to what the template wants.
182
+ includeDom: input.includeHtml,
183
+ includeScreenshot: input.captureScreenshots,
184
+ // Keep the original keys too for any downstream node that reads them
185
+ // (cheap to send, future-proof against template field-name churn).
165
186
  includeHtml: input.includeHtml,
166
187
  captureScreenshots: input.captureScreenshots,
167
188
  };
@@ -182,7 +203,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
182
203
  }
183
204
  if (!progressCallback)
184
205
  return;
185
- const completedNodes = (exec.nodeExecutions ?? []).filter(n => n.nodeType === 'page.capture' && n.status === 'success').length;
206
+ const completedNodes = (exec.nodeExecutions ?? []).filter(n => n.nodeType === 'browser.capture' && n.status === 'success').length;
186
207
  if (completedNodes !== lastCompleted) {
187
208
  lastCompleted = completedNodes;
188
209
  await progressCallback({
@@ -195,22 +216,35 @@ export async function probePageHandler(input, context, rawProgressCallback) {
195
216
  // ── Format response ────────────────────────────────────────────────────
196
217
  const duration = Date.now() - startTime;
197
218
  const captureNodes = (finalExecution.nodeExecutions ?? [])
198
- .filter(n => n.nodeType === 'page.capture')
219
+ .filter(n => n.nodeType === 'browser.capture')
199
220
  .sort((a, b) => a.executionOrder - b.executionOrder);
200
221
  const results = [];
201
- const screenshotBlocks = [];
202
222
  for (let i = 0; i < input.targets.length; i++) {
203
223
  const target = input.targets[i];
204
224
  const node = captureNodes[i];
205
225
  const data = node?.outputData ?? {};
226
+ // Backend (post-154e1e69) emits browser.capture output_data with:
227
+ // captured_url, status_code, title, load_time_ms,
228
+ // console_slice (already per-capture, in {text, level, location, timestamp} shape),
229
+ // network_summary (already pre-aggregated by FULL URL,
230
+ // in {url, count, methods[], statuses{}, resource_types[]} shape),
231
+ // surfer_page_uuid (reference to SurferPage row for screenshot/title/visible_text),
232
+ // error
233
+ // axiosTransport snake→camel'd at the wire, so JS-side these are
234
+ // capturedUrl / consoleSlice / networkSummary / surferPageUuid / etc.
235
+ // Re-aggregate networkSummary by origin+pathname so refetch loops
236
+ // collapse (preserves the original client-feedback contract).
206
237
  const result = {
207
238
  url: target.url, // ORIGINAL caller URL — not the tunneled rewrite
208
- finalUrl: typeof data.finalUrl === 'string' ? data.finalUrl : (typeof data.url === 'string' ? data.url : target.url),
239
+ finalUrl: typeof data.capturedUrl === 'string' ? data.capturedUrl
240
+ : typeof data.finalUrl === 'string' ? data.finalUrl
241
+ : typeof data.url === 'string' ? data.url
242
+ : target.url,
209
243
  statusCode: typeof data.statusCode === 'number' ? data.statusCode : 0,
210
244
  title: typeof data.title === 'string' ? data.title : null,
211
245
  loadTimeMs: typeof data.loadTimeMs === 'number' ? data.loadTimeMs : 0,
212
- consoleErrors: summarizeConsole(Array.isArray(data.consoleSlice) ? data.consoleSlice : []),
213
- networkSummary: summarizeHar(Array.isArray(data.harSlice) ? data.harSlice : []),
246
+ consoleErrors: mapConsoleSlice(Array.isArray(data.consoleSlice) ? data.consoleSlice : []),
247
+ networkSummary: reaggregateByOriginPath(Array.isArray(data.networkSummary) ? data.networkSummary : []),
214
248
  };
215
249
  if (input.includeHtml && typeof data.html === 'string') {
216
250
  result.html = data.html;
@@ -218,10 +252,16 @@ export async function probePageHandler(input, context, rawProgressCallback) {
218
252
  if (typeof data.error === 'string' && data.error) {
219
253
  result.error = data.error;
220
254
  }
221
- results.push(result);
222
- if (input.captureScreenshots && typeof data.screenshotB64 === 'string' && data.screenshotB64) {
223
- screenshotBlocks.push(imageContentBlock(data.screenshotB64, 'image/png'));
255
+ if (typeof data.surferPageUuid === 'string' && data.surferPageUuid) {
256
+ result.surferPageUuid = data.surferPageUuid;
224
257
  }
258
+ results.push(result);
259
+ // Backend stores screenshots on the SurferPage row referenced by
260
+ // surfer_page_uuid; the inline screenshotB64 is no longer in capture
261
+ // output. v1: skip the per-result image content block; the screenshot
262
+ // is reachable via search_executions detail's surfer_page_uuid → SurferPage.
263
+ // (Future enhancement: fetch the SurferPage's presigned screenshot_url
264
+ // when input.captureScreenshots is true.)
225
265
  }
226
266
  const responsePayload = {
227
267
  executionId: executionUuid,
@@ -247,7 +287,6 @@ export async function probePageHandler(input, context, rawProgressCallback) {
247
287
  return {
248
288
  content: [
249
289
  { type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) },
250
- ...screenshotBlocks,
251
290
  ],
252
291
  };
253
292
  }
@@ -7,6 +7,94 @@
7
7
  * Pure functions — no I/O, no async — so they can be reused by the future
8
8
  * `summarize_execution` tool.
9
9
  */
10
+ /**
11
+ * Re-aggregate backend's pre-grouped network_summary entries by
12
+ * `origin + pathname` (vs backend's full-URL key). Collapses refetch loops:
13
+ * 5 separate `/api/poll?n=0..4` entries become 1 entry with count: 5.
14
+ *
15
+ * Backend `browser.capture` (commit 154e1e69) emits network_summary already
16
+ * grouped by full URL with shape `{url, count, methods[], statuses{}, resource_types[]}`.
17
+ * That preserves the per-request granularity but defeats the original
18
+ * client #1 use case ("endpoint hit N times" refetch detection). MCP-side
19
+ * re-aggregation runs once over the small pre-grouped list — cheap.
20
+ */
21
+ export function reaggregateByOriginPath(entries) {
22
+ if (!Array.isArray(entries))
23
+ return [];
24
+ const buckets = new Map();
25
+ for (const e of entries) {
26
+ try {
27
+ const url = e?.url;
28
+ if (typeof url !== 'string')
29
+ continue;
30
+ const parsed = new URL(url);
31
+ const key = `${parsed.origin}${parsed.pathname}`;
32
+ const count = typeof e.count === 'number' ? e.count : 0;
33
+ const statuses = e.statuses ?? {};
34
+ const existing = buckets.get(key);
35
+ if (existing) {
36
+ existing.count += count;
37
+ for (const [code, n] of Object.entries(statuses)) {
38
+ if (typeof n === 'number') {
39
+ existing.statuses[code] = (existing.statuses[code] ?? 0) + n;
40
+ }
41
+ }
42
+ }
43
+ else {
44
+ const out = {
45
+ url: key,
46
+ count,
47
+ statuses: { ...statuses },
48
+ totalBytes: 0, // Backend's pre-grouped shape doesn't expose response bytes; placeholder until we wire fetched-bytes.
49
+ };
50
+ buckets.set(key, out);
51
+ }
52
+ }
53
+ catch {
54
+ // malformed URL — skip
55
+ }
56
+ }
57
+ return [...buckets.values()].sort((a, b) => b.count - a.count);
58
+ }
59
+ /**
60
+ * Map backend's console_slice entry shape to MCP's ConsoleErrorEntry.
61
+ * Backend shape: {text, level, location: {url, line}, timestamp}
62
+ * MCP shape: {level, text, source?, lineNumber?, timestamp?}
63
+ */
64
+ export function mapConsoleSlice(entries) {
65
+ if (!Array.isArray(entries))
66
+ return [];
67
+ const out = [];
68
+ for (const e of entries) {
69
+ if (typeof e !== 'object' || e === null)
70
+ continue;
71
+ const entry = {
72
+ level: typeof e.level === 'string' ? e.level : 'log',
73
+ text: typeof e.text === 'string' ? e.text : '',
74
+ };
75
+ const loc = e.location ?? {};
76
+ if (typeof loc.url === 'string' && loc.url)
77
+ entry.source = loc.url;
78
+ else if (typeof e.source === 'string' && e.source)
79
+ entry.source = e.source;
80
+ if (typeof loc.line === 'number')
81
+ entry.lineNumber = loc.line;
82
+ else if (typeof e.lineNumber === 'number')
83
+ entry.lineNumber = e.lineNumber;
84
+ // Backend timestamps are ISO strings; MCP type uses number (ms since epoch).
85
+ // Coerce when possible; otherwise pass through unchanged.
86
+ if (typeof e.timestamp === 'number') {
87
+ entry.timestamp = e.timestamp;
88
+ }
89
+ else if (typeof e.timestamp === 'string') {
90
+ const parsed = Date.parse(e.timestamp);
91
+ if (!isNaN(parsed))
92
+ entry.timestamp = parsed;
93
+ }
94
+ out.push(entry);
95
+ }
96
+ return out;
97
+ }
10
98
  /**
11
99
  * Aggregate HAR `log.entries` into per-endpoint NetworkSummary[], sorted
12
100
  * descending by request count (hottest endpoints first). Malformed entries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {