@debugg-ai/debugg-ai-mcp 4.2.0 → 4.2.2

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.
package/README.md CHANGED
@@ -142,13 +142,13 @@ Fires a server-side browser-agent crawl to populate the project's knowledge grap
142
142
 
143
143
  #### `probe_page`
144
144
 
145
- **Lightweight no-LLM batch page probe.** Pass 1-20 URLs; each navigates, waits for load, and returns rendered state — screenshot + page metadata + structured console errors + network summary. No agent loop, no LLM cost, no scenario assertions. Use it for "did I just break /settings?", multi-route smoke after a refactor, CI per-PR sweeps, and quick is-it-up checks where `check_app_in_browser`'s 60-150s agent loop is overkill.
145
+ **Lightweight no-LLM batch page probe.** Pass 1-20 URLs; each navigates, settles on content (the DOM going quiet, bounded — never on network silence, which a live app never reaches), and returns rendered state — screenshot + page metadata + structured console errors + network summary. No agent loop, no LLM cost, no scenario assertions. Use it for "did I just break /settings?", multi-route smoke after a refactor, CI per-PR sweeps, and quick is-it-up checks where `check_app_in_browser`'s 60-150s agent loop is overkill.
146
146
 
147
147
  | Parameter | Type | Description |
148
148
  |-----------|------|-------------|
149
149
  | `targets` | array **required** | 1-20 entries: `[{url, waitForSelector?, waitForLoadState?, timeoutMs?}]` |
150
150
  | `targets[].url` | string **required** | Public URL or localhost (auto-tunneled) |
151
- | `targets[].waitForLoadState` | enum | `'load'` (default) / `'domcontentloaded'` / `'networkidle'` |
151
+ | `targets[].waitForLoadState` | enum | `'domcontentloaded'` (default, + a bounded content settle) / `'load'` (also blocks on third-party embeds) / `'networkidle'` (accepted, never issued — a live site's network does not go idle) |
152
152
  | `targets[].waitForSelector` | string | Optional CSS selector to wait for after navigation |
153
153
  | `targets[].timeoutMs` | number | Per-URL timeout, 1000-30000 (default 10000) |
154
154
  | `includeHtml` | boolean | Return raw HTML in each result (default false) |
@@ -12,7 +12,7 @@ import { DebuggAIServerClient } from '../services/index.js';
12
12
  import { getEvalTemplateSlug } from '../services/workflows.js';
13
13
  import { adaptVerdict, isEnvironmentDefault } from '../services/verdictAdapter.js';
14
14
  import { TunnelProvisionError } from '../services/tunnels.js';
15
- import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, acquirePortRoute, releasePortRoute, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
15
+ import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, acquirePortRoute, releasePortRoute, sanitizeResponseUrls, touchTunnelById, retargetAuxiliaryUrl, } from '../utils/tunnelContext.js';
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import { detectRepoName } from '../utils/gitContext.js';
18
18
  import { disposeUnhealthyTunnel } from '../utils/tunnelDisposition.js';
@@ -379,10 +379,33 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
379
379
  auth.environmentId = input.auth.environmentId;
380
380
  if (input.auth.precondition)
381
381
  auth.precondition = input.auth.precondition;
382
- if (input.auth.entryUrl)
383
- auth.entryUrl = input.auth.entryUrl;
384
- if (input.auth.deepUrl)
385
- auth.deepUrl = input.auth.deepUrl;
382
+ // Bead go1m: entryUrl/deepUrl are URLs the run NAVIGATES, so they need the
383
+ // same localhost→tunnel rewrite `url` gets (contextData.targetUrl above).
384
+ // Forwarded verbatim they reached the remote browser as literal localhost
385
+ // and it dialled its own loopback: offscope_host + ERR_CONNECTION_REFUSED.
386
+ for (const field of ['entryUrl', 'deepUrl']) {
387
+ const supplied = input.auth[field];
388
+ if (!supplied)
389
+ continue;
390
+ const rewrite = retargetAuxiliaryUrl(ctx, supplied);
391
+ if (!rewrite.ok) {
392
+ const payload = {
393
+ error: 'AuthUrlPortMismatch',
394
+ message: `auth.${field} points at localhost:${rewrite.port} but url points at ` +
395
+ `localhost:${rewrite.primaryPort}. A single call tunnels exactly one local ` +
396
+ `port, so the remote browser cannot reach both. Put the login page and the ` +
397
+ `target page on the same port, or drop auth.${field} and pass username/password ` +
398
+ `at the top level so the agent signs in on the page it already reached.`,
399
+ detail: { field, authPort: rewrite.port, urlPort: rewrite.primaryPort, url: originalUrl },
400
+ };
401
+ logger.warn(`check_app_in_browser: ${payload.message}`);
402
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
403
+ }
404
+ auth[field] = rewrite.url;
405
+ if (rewrite.rewritten) {
406
+ logger.info(`check_app_in_browser: tunneled auth.${field} ${supplied} -> ${rewrite.url}`);
407
+ }
408
+ }
386
409
  // WHICH account the precondition logs in as. Absent → the environment's
387
410
  // default credential (the pre-existing behaviour, correct for a caller
388
411
  // that named nobody).
@@ -22,6 +22,8 @@ LOCALHOST SUPPORT: any localhost URL is auto-tunneled. Pre-flight TCP probe fail
22
22
 
23
23
  BATCH MODE: pass up to 20 targets in one call to share browser session + tunnel — dramatically faster than firing parallel single-URL probes (one execution unit, not N). Per-URL waitForSelector / waitForLoadState / timeoutMs override defaults.
24
24
 
25
+ READINESS: navigation settles on CONTENT (the page's DOM going quiet), bounded — not on network silence, which never arrives on a live app, and not on 'load', which blocks on third-party embeds. The default is right for SPAs; reach for waitForSelector, not waitForLoadState, when you need to wait for something specific.
26
+
25
27
  A single failed target's error appears in result.error without failing the whole batch — the other results stay valid.`;
26
28
  const TARGET_PROPERTIES = {
27
29
  url: {
@@ -35,11 +37,15 @@ const TARGET_PROPERTIES = {
35
37
  waitForLoadState: {
36
38
  type: 'string',
37
39
  enum: ['load', 'domcontentloaded', 'networkidle'],
38
- description: "When to consider the page 'loaded' before capturing. Default 'load'. Use 'networkidle' for SPAs to wait until the bundle finishes rendering.",
40
+ // sentinal-kvoou. This description used to read "Default 'load'. Use 'networkidle'
41
+ // for SPAs to wait until the bundle finishes rendering" — advice that hangs on
42
+ // exactly the class of app it names. See __tests__/tools/probePageWaitContract.ts
43
+ // for the measurement against https://debugg.ai.
44
+ description: "When to consider the page ready to capture. Default 'domcontentloaded', followed by a bounded content settle (the page's DOM going quiet) — that is what actually makes a client-rendered SPA safe to screenshot, and it needs no override. Only override for a specific reason: 'load' additionally blocks on every sub-resource, including third-party iframes and images we do not control, so a slow embed can time the whole probe out. 'networkidle' is accepted for compatibility but is never issued — a live site's network does not go idle (analytics, polling, websockets, ads) — and behaves as 'domcontentloaded'. To wait on something specific, use waitForSelector.",
39
45
  },
40
46
  timeoutMs: {
41
47
  type: 'number',
42
- description: 'Per-URL navigation timeout in milliseconds (1000-30000, default 10000).',
48
+ description: "Per-URL budget in milliseconds for navigating AND settling this target (1000-30000, default 10000). The content settle spends what the navigation left over, so this is the whole cost of the target, not just the goto.",
43
49
  },
44
50
  };
45
51
  export function buildProbePageTool() {
@@ -226,7 +226,15 @@ export var LogLevel;
226
226
  export const ProbePageTargetSchema = z.object({
227
227
  url: z.preprocess(normalizeUrl, z.string().url('Invalid URL. Pass a full URL like "http://localhost:3000" or "https://example.com". Localhost URLs are auto-tunneled to the remote browser.')),
228
228
  waitForSelector: z.string().optional(),
229
- waitForLoadState: z.enum(['load', 'domcontentloaded', 'networkidle']).default('load'),
229
+ // sentinal-kvoou: 'domcontentloaded', NOT 'load'. 'load' blocks on every
230
+ // sub-resource including third-party iframes we do not control — measured against
231
+ // https://debugg.ai (two YouTube embeds), the 'load' default timed the goto out at
232
+ // 10s and reported statusCode 0 on a page that renders in 3.5s. Readiness comes from
233
+ // the backend's bounded CONTENT settle after the goto, not from a load state.
234
+ // 'networkidle' stays in the enum (this package is published; removing it would turn
235
+ // an existing caller's probe into a hard validation error) and is neutralized
236
+ // server-side — never a wait, always domcontentloaded + settle.
237
+ waitForLoadState: z.enum(['load', 'domcontentloaded', 'networkidle']).default('domcontentloaded'),
230
238
  timeoutMs: z.number().int().min(1000, 'timeoutMs minimum is 1000 (1s)').max(30000, 'timeoutMs maximum is 30000 (30s) — longer probes should use check_app_in_browser').default(10000),
231
239
  }).strict();
232
240
  export const ProbePageInputSchema = z.object({
@@ -69,6 +69,46 @@ export async function ensureTunnel(ctx, tunnelKey, tunnelId, keyId, revokeKey) {
69
69
  const info = await tunnelManager.ensureSessionTunnel(getSessionKey(), tunnelKey, tunnelId, keyId, revokeKey);
70
70
  return { ...ctx, tunnelId: info.tunnelId, targetUrl: retargetTunnelUrl(info.tunnelUrl, ctx.originalUrl) };
71
71
  }
72
+ /**
73
+ * Retarget an auxiliary URL the CALLER supplied (`auth.entryUrl`, `auth.deepUrl`)
74
+ * onto the same tunnel the run's primary `url` already uses.
75
+ *
76
+ * Bead go1m: only the top-level `url` ever went through the localhost→tunnel
77
+ * rewrite. `auth.entryUrl`/`auth.deepUrl` were forwarded verbatim, so a
78
+ * localhost entry URL reached the REMOTE browser as a literal `localhost:PORT`
79
+ * and it dialled its own loopback — `offscope_host` + ERR_CONNECTION_REFUSED on
80
+ * the documented "log in THEN deep-navigate" path. Every URL the run may
81
+ * navigate has to go through the same rewrite as `url`.
82
+ *
83
+ * Three cases, deliberately distinguished:
84
+ * - not localhost (public URL, or dev mode / no tunnel) → returned unchanged.
85
+ * Matches how the primary `url` is treated in those same conditions.
86
+ * - localhost on the SAME port as the primary URL → retargeted onto the
87
+ * tunnel origin, preserving path + query + hash (`retargetTunnelUrl`).
88
+ * - localhost on a DIFFERENT port → REFUSED, never silently retargeted. The
89
+ * session's single Caddy upstream is pointed at the primary URL's port for
90
+ * the whole call (see `acquirePortRoute`), so rewriting a cross-port URL
91
+ * onto the same origin would send the login navigation to whichever port
92
+ * Caddy happens to hold — the silent misdirection the port lock exists to
93
+ * prevent. Fail fast and tell the caller instead.
94
+ */
95
+ export function retargetAuxiliaryUrl(ctx, auxUrl) {
96
+ // No tunnel in play (public target, or dev mode where the backend reaches
97
+ // localhost directly): the primary URL isn't rewritten either, so neither is this.
98
+ if (!ctx.isLocalhost || !ctx.tunnelId || !ctx.targetUrl) {
99
+ return { ok: true, url: auxUrl, rewritten: false };
100
+ }
101
+ // A public auxiliary URL is reachable by the remote browser as-is.
102
+ if (!isLocalhostUrl(auxUrl)) {
103
+ return { ok: true, url: auxUrl, rewritten: false };
104
+ }
105
+ const port = extractLocalhostPort(auxUrl);
106
+ const primaryPort = extractLocalhostPort(ctx.originalUrl);
107
+ if (port !== undefined && primaryPort !== undefined && port !== primaryPort) {
108
+ return { ok: false, reason: 'port_mismatch', port, primaryPort };
109
+ }
110
+ return { ok: true, url: retargetTunnelUrl(ctx.targetUrl, auxUrl), rewritten: true };
111
+ }
72
112
  // ─── Port route lock (§2.4) ──────────────────────────────────────────────────
73
113
  /**
74
114
  * Acquire this session's shared Caddy route for `ctx`'s port, blocking until
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "caddy": "2.11.3",