@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](
|
|
10
|
+
* browser.setup → loop[targets](browser.navigate → browser.capture) → done
|
|
11
11
|
*
|
|
12
|
-
* Each
|
|
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 {
|
|
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
|
-
|
|
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 === '
|
|
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 === '
|
|
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.
|
|
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:
|
|
213
|
-
networkSummary:
|
|
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
|
-
|
|
222
|
-
|
|
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
|