@debugg-ai/debugg-ai-mcp 1.0.40 → 1.0.42

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.
@@ -9,6 +9,7 @@ import { handleExternalServiceError } from '../utils/errors.js';
9
9
  import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
10
10
  import { DebuggAIServerClient } from '../services/index.js';
11
11
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
12
+ import { tunnelManager } from '../services/ngrok/tunnelManager.js';
12
13
  const logger = new Logger({ module: 'testPageChangesHandler' });
13
14
  // Cache the template UUID and project UUIDs within a server session to avoid re-fetching
14
15
  let cachedTemplateUuid = null;
@@ -168,10 +169,10 @@ export async function testPageChangesHandler(input, context, progressCallback) {
168
169
  if (stepsTaken > 0) {
169
170
  // Extract the latest brain.step to show what the agent is doing
170
171
  const latestStep = (exec.nodeExecutions ?? [])
171
- .filter(n => n.nodeType === 'brain.step' && n.outputData?.decision)
172
+ .filter(n => n.nodeType === 'brain.step' && n.outputData)
172
173
  .sort((a, b) => b.executionOrder - a.executionOrder)[0];
173
- if (latestStep?.outputData?.decision) {
174
- const d = latestStep.outputData.decision;
174
+ const d = latestStep?.outputData?.decision ?? latestStep?.outputData;
175
+ if (d) {
175
176
  const action = d.actionType ?? d.action_type ?? 'working';
176
177
  const intent = d.intent;
177
178
  message = intent
@@ -196,15 +197,27 @@ export async function testPageChangesHandler(input, context, progressCallback) {
196
197
  }
197
198
  }, abortController.signal);
198
199
  const duration = Date.now() - startTime;
200
+ // If the execution failed because the tunnel URL was unreachable, evict the dead tunnel
201
+ // so the next call re-provisions a fresh one instead of reusing a dead entry.
202
+ const tunnelErrorMsg = finalExecution.errorMessage ?? finalExecution.state?.error ?? '';
203
+ if (ctx.tunnelId && tunnelErrorMsg.includes('unreachable') && tunnelErrorMsg.includes('ngrok')) {
204
+ logger.warn(`Tunnel ${ctx.tunnelId} appears dead (unreachable) — evicting from cache`);
205
+ tunnelManager.stopTunnel(ctx.tunnelId).catch(() => { });
206
+ ctx = { ...ctx, tunnelId: undefined };
207
+ }
199
208
  // --- Format result ---
200
209
  const outcome = finalExecution.state?.outcome ?? finalExecution.status;
201
210
  const nodes = finalExecution.nodeExecutions ?? [];
202
- // Extract step-by-step action trace from brain.step nodes
211
+ // subworkflow.run is the current graph shape — carries outcome, actionHistory, screenshot
212
+ const subworkflowNode = nodes.find(n => n.nodeType === 'subworkflow.run');
213
+ // surfer.execute_task and brain.step/brain.evaluate are older graph shapes
214
+ const surferNode = nodes.find(n => n.nodeType === 'surfer.execute_task');
215
+ // Action trace: brain.step nodes (old) → subworkflow.run actionHistory (new)
203
216
  const brainSteps = nodes
204
- .filter(n => n.nodeType === 'brain.step' && n.outputData?.decision)
217
+ .filter(n => n.nodeType === 'brain.step' && n.outputData)
205
218
  .sort((a, b) => a.executionOrder - b.executionOrder);
206
219
  const actionTrace = brainSteps.map((n, i) => {
207
- const d = n.outputData.decision;
220
+ const d = n.outputData.decision ?? n.outputData;
208
221
  return {
209
222
  step: i + 1,
210
223
  action: d.actionType ?? d.action_type,
@@ -215,33 +228,52 @@ export async function testPageChangesHandler(input, context, progressCallback) {
215
228
  durationMs: n.executionTimeMs,
216
229
  };
217
230
  });
218
- // Extract evaluation from brain.evaluate node
231
+ const subworkflowHistory = subworkflowNode?.outputData?.actionHistory;
232
+ if (actionTrace.length === 0 && Array.isArray(subworkflowHistory) && subworkflowHistory.length > 0) {
233
+ subworkflowHistory.forEach((step, i) => {
234
+ actionTrace.push({
235
+ step: i + 1,
236
+ action: step.actionType ?? step.action_type ?? step.action,
237
+ intent: step.intent,
238
+ target: step.target,
239
+ value: step.value ?? undefined,
240
+ success: step.success ?? true,
241
+ durationMs: step.durationMs ?? step.duration_ms ?? undefined,
242
+ });
243
+ });
244
+ }
245
+ // Evaluation: brain.evaluate (old) → subworkflow.run outcome/success (new)
219
246
  const evalNode = nodes.find(n => n.nodeType === 'brain.evaluate');
220
- const evaluation = evalNode?.outputData ? {
221
- passed: evalNode.outputData.passed,
222
- outcome: evalNode.outputData.outcome,
223
- reason: evalNode.outputData.reason,
224
- verifications: evalNode.outputData.verifications,
225
- } : undefined;
226
- // Also check for surfer.execute_task (older workflow graphs)
227
- const surferNode = nodes.find(n => n.nodeType === 'surfer.execute_task');
247
+ let evaluation;
248
+ if (evalNode?.outputData) {
249
+ evaluation = {
250
+ passed: evalNode.outputData.passed,
251
+ outcome: evalNode.outputData.outcome,
252
+ reason: evalNode.outputData.reason,
253
+ verifications: evalNode.outputData.verifications,
254
+ };
255
+ }
256
+ else if (subworkflowNode?.outputData) {
257
+ const sw = subworkflowNode.outputData;
258
+ evaluation = {
259
+ passed: sw.success,
260
+ outcome: sw.outcome,
261
+ reason: sw.error || undefined,
262
+ };
263
+ }
228
264
  const responsePayload = {
229
265
  outcome,
230
- success: finalExecution.state?.success ?? false,
266
+ success: finalExecution.state?.success ?? subworkflowNode?.outputData?.success ?? false,
231
267
  status: finalExecution.status,
232
- stepsTaken: finalExecution.state?.stepsTaken ?? actionTrace.length ?? 0,
268
+ stepsTaken: finalExecution.state?.stepsTaken ?? subworkflowNode?.outputData?.stepsTaken ?? actionTrace.length,
233
269
  targetUrl: originalUrl,
234
270
  executionId: executionUuid,
235
271
  durationMs: finalExecution.durationMs ?? duration,
236
272
  };
237
- // The step-by-step action trace — what the browser agent did and why
238
- if (actionTrace.length > 0) {
273
+ if (actionTrace.length > 0)
239
274
  responsePayload.actionTrace = actionTrace;
240
- }
241
- // The final evaluation — pass/fail with reasoning
242
- if (evaluation) {
275
+ if (evaluation)
243
276
  responsePayload.evaluation = evaluation;
244
- }
245
277
  if (finalExecution.state?.error)
246
278
  responsePayload.agentError = finalExecution.state.error;
247
279
  if (finalExecution.errorMessage)
@@ -262,15 +294,23 @@ export async function testPageChangesHandler(input, context, progressCallback) {
262
294
  const content = [
263
295
  { type: 'text', text: JSON.stringify(responsePayload, null, 2) },
264
296
  ];
265
- // Search all node outputs for screenshot/gif URLs not just the surfer node
266
- const SCREENSHOT_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
297
+ // Screenshot: check for already-base64 field first (subworkflow.run), then URL-based fields
298
+ const SCREENSHOT_URL_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
267
299
  const GIF_KEYS = ['runGif', 'gifUrl', 'gif', 'videoUrl', 'recordingUrl'];
268
- let screenshotUrl = null;
300
+ let screenshotEmbedded = false;
269
301
  let gifUrl = null;
270
- for (const node of finalExecution.nodeExecutions ?? []) {
302
+ // subworkflow.run carries screenshotB64 directly no fetch needed
303
+ const screenshotB64 = subworkflowNode?.outputData?.screenshotB64;
304
+ if (typeof screenshotB64 === 'string' && screenshotB64) {
305
+ logger.info('Embedding inline base64 screenshot from subworkflow.run');
306
+ content.push(imageContentBlock(screenshotB64, 'image/png'));
307
+ screenshotEmbedded = true;
308
+ }
309
+ let screenshotUrl = null;
310
+ for (const node of nodes) {
271
311
  const data = node.outputData ?? {};
272
- if (!screenshotUrl) {
273
- for (const key of SCREENSHOT_KEYS) {
312
+ if (!screenshotEmbedded && !screenshotUrl) {
313
+ for (const key of SCREENSHOT_URL_KEYS) {
274
314
  if (typeof data[key] === 'string' && data[key]) {
275
315
  screenshotUrl = data[key];
276
316
  break;
@@ -285,10 +325,10 @@ export async function testPageChangesHandler(input, context, progressCallback) {
285
325
  }
286
326
  }
287
327
  }
288
- if (screenshotUrl && gifUrl)
328
+ if ((screenshotEmbedded || screenshotUrl) && gifUrl)
289
329
  break;
290
330
  }
291
- if (screenshotUrl) {
331
+ if (!screenshotEmbedded && screenshotUrl) {
292
332
  logger.info(`Embedding screenshot: ${screenshotUrl}`);
293
333
  const img = await fetchImageAsBase64(screenshotUrl).catch(() => null);
294
334
  if (img)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {