@debugg-ai/debugg-ai-mcp 3.6.0 → 3.7.1

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.
@@ -23,10 +23,10 @@ import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js
23
23
  import { extractLocalhostPort } from '../utils/urlParser.js';
24
24
  import { buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
25
25
  import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
26
+ import { getPageProbeTemplateSlug } from '../services/workflows.js';
26
27
  import { reaggregateByOriginPath, mapConsoleSlice } from '../utils/harSummarizer.js';
27
28
  import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
28
29
  const logger = new Logger({ module: 'probePageHandler' });
29
- const TEMPLATE_KEYWORD = 'page probe';
30
30
  export async function probePageHandler(input, context, rawProgressCallback) {
31
31
  const startTime = Date.now();
32
32
  logger.toolStart('probe_page', input);
@@ -49,12 +49,25 @@ export async function probePageHandler(input, context, rawProgressCallback) {
49
49
  : undefined;
50
50
  const client = new DebuggAIServerClient(config.api.key);
51
51
  await client.init();
52
+ // Bead 56kd.7: cancellation is driven by the MCP request/transport lifecycle
53
+ // (context.signal), NOT process.stdin. The SDK aborts context.signal when the
54
+ // client cancels the call OR the transport closes — e.g. an HTTP client drops
55
+ // the connection. Under the stateless HTTP transport that is the ONLY signal
56
+ // we get: stdin is not the transport, so the old stdin 'close' listener never
57
+ // fired and a dropped client kept polling for up to ~10 min. Wiring to
58
+ // context.signal cancels the poll immediately (parity with 56kd.5).
52
59
  const abortController = new AbortController();
53
- const onStdinClose = () => {
60
+ const onAbort = () => {
54
61
  abortController.abort();
55
- progressDisabled = true;
62
+ progressDisabled = true; // client is gone — stop emitting
56
63
  };
57
- process.stdin.once('close', onStdinClose);
64
+ const requestSignal = context.signal;
65
+ if (requestSignal) {
66
+ if (requestSignal.aborted)
67
+ onAbort();
68
+ else
69
+ requestSignal.addEventListener('abort', onAbort, { once: true });
70
+ }
58
71
  // Per-target tunnel contexts. Index aligns with input.targets[].
59
72
  const targetContexts = [];
60
73
  // Tunnel keys we provisioned this call (for cleanup if creation fails after key acquired).
@@ -154,12 +167,16 @@ export async function probePageHandler(input, context, rawProgressCallback) {
154
167
  if (progressCallback) {
155
168
  await progressCallback({ progress: ++progressStep, total: TOTAL_STEPS, message: 'Locating page-probe workflow template...' });
156
169
  }
157
- const templateUuid = await getCachedTemplateUuid(TEMPLATE_KEYWORD, async (name) => {
158
- return client.workflows.findTemplateByName(name);
170
+ // Pin dispatch to the stable slug (bead 56kd.8) no fuzzy name resolution.
171
+ // Cache key = the slug so the key and the lookup can never drift apart.
172
+ const templateSlug = getPageProbeTemplateSlug();
173
+ const templateUuid = await getCachedTemplateUuid(templateSlug, async () => {
174
+ return client.workflows.findTemplateBySlug(templateSlug);
159
175
  });
160
176
  if (!templateUuid) {
161
- throw new Error(`Page Probe Workflow Template not found. ` +
162
- `Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
177
+ throw new Error(`Page Probe Workflow Template not found (slug "${templateSlug}"). ` +
178
+ `Ensure the backend has that template seeded and accessible ` +
179
+ `(GET /api/v1/workflows/?slug=${templateSlug}).`);
163
180
  }
164
181
  // ── Build contextData (camelCase; axiosTransport snake_cases on the wire) ──
165
182
  // Backend's browser.setup node (shared with App Evaluation + Raw Crawl
@@ -325,7 +342,8 @@ export async function probePageHandler(input, context, rawProgressCallback) {
325
342
  throw handleExternalServiceError(error, 'DebuggAI', 'probe_page execution');
326
343
  }
327
344
  finally {
328
- process.stdin.removeListener('close', onStdinClose);
345
+ if (requestSignal)
346
+ requestSignal.removeEventListener('abort', onAbort);
329
347
  // Tunnels intentionally NOT torn down — reuse pattern (bead vwd) +
330
348
  // 55-min idle auto-shutoff. Revoke only orphaned keys (we acquired the
331
349
  // key but tunnel creation failed before ensureTunnel completed).
@@ -266,6 +266,23 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
266
266
  contextData.projectId = projectUuid;
267
267
  }
268
268
  contextData.headless = true; // D7: the MCP always runs headless — no opt-out.
269
+ // Bead 56kd.6: forward the auth-precondition deep-link intent verbatim per
270
+ // backend contract sentinal-k8x1f.8 (contextData.auth). Thin relay — express
271
+ // intent + forward; the backend authenticates then navigates to deepUrl. Only
272
+ // the fields the caller set are sent (camelCase here → snake_case on the wire).
273
+ if (input.auth) {
274
+ const auth = {};
275
+ if (input.auth.environmentId)
276
+ auth.environmentId = input.auth.environmentId;
277
+ if (input.auth.precondition)
278
+ auth.precondition = input.auth.precondition;
279
+ if (input.auth.entryUrl)
280
+ auth.entryUrl = input.auth.entryUrl;
281
+ if (input.auth.deepUrl)
282
+ auth.deepUrl = input.auth.deepUrl;
283
+ if (Object.keys(auth).length > 0)
284
+ contextData.auth = auth;
285
+ }
269
286
  // --- Build env (credentials/environment) ---
270
287
  const env = {};
271
288
  if (input.environmentId)
@@ -20,10 +20,10 @@ import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js
20
20
  import { extractLocalhostPort } from '../utils/urlParser.js';
21
21
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
22
22
  import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
23
+ import { getCrawlTemplateSlug } from '../services/workflows.js';
23
24
  import { isTransientWorkflowError, transientReasonTag } from '../utils/transientErrors.js';
24
25
  import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
25
26
  const logger = new Logger({ module: 'triggerCrawlHandler' });
26
- const TEMPLATE_KEYWORD = 'raw crawl';
27
27
  // Bead kbo9: same env-driven retry budget as testPageChangesHandler (kbxy).
28
28
  function getMaxTransientRetries() {
29
29
  const raw = process.env.DEBUGGAI_TRANSIENT_RETRIES;
@@ -59,12 +59,25 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
59
59
  const originalUrl = resolveTargetUrl(input);
60
60
  let ctx = buildContext(originalUrl);
61
61
  let keyId;
62
+ // Bead 56kd.7: cancellation is driven by the MCP request/transport lifecycle
63
+ // (context.signal), NOT process.stdin. The SDK aborts context.signal when the
64
+ // client cancels the call OR the transport closes — e.g. an HTTP client drops
65
+ // the connection. Under the stateless HTTP transport that is the ONLY signal
66
+ // we get: stdin is not the transport, so the old stdin 'close' listener never
67
+ // fired and a dropped client kept polling for up to ~10 min. Wiring to
68
+ // context.signal cancels the poll immediately (parity with 56kd.5).
62
69
  const abortController = new AbortController();
63
- const onStdinClose = () => {
70
+ const onAbort = () => {
64
71
  abortController.abort();
65
- progressDisabled = true;
72
+ progressDisabled = true; // client is gone — stop emitting
66
73
  };
67
- process.stdin.once('close', onStdinClose);
74
+ const requestSignal = context.signal;
75
+ if (requestSignal) {
76
+ if (requestSignal.aborted)
77
+ onAbort();
78
+ else
79
+ requestSignal.addEventListener('abort', onAbort, { once: true });
80
+ }
68
81
  try {
69
82
  // --- Tunnel: reuse existing or provision a fresh one ---
70
83
  if (ctx.isLocalhost) {
@@ -137,12 +150,16 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
137
150
  if (progressCallback) {
138
151
  await progressCallback({ progress: 2, total: 4, message: 'Locating crawl workflow template...' });
139
152
  }
140
- const templateUuid = await getCachedTemplateUuid(TEMPLATE_KEYWORD, async (name) => {
141
- return client.workflows.findTemplateByName(name);
153
+ // Pin dispatch to the stable slug (bead 56kd.8) no fuzzy name resolution.
154
+ // Cache key = the slug so the key and the lookup can never drift apart.
155
+ const templateSlug = getCrawlTemplateSlug();
156
+ const templateUuid = await getCachedTemplateUuid(templateSlug, async () => {
157
+ return client.workflows.findTemplateBySlug(templateSlug);
142
158
  });
143
159
  if (!templateUuid) {
144
- throw new Error(`Raw Crawl Workflow Template not found. ` +
145
- `Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
160
+ throw new Error(`Crawl Workflow Template not found (slug "${templateSlug}"). ` +
161
+ `Ensure the backend has that template seeded and accessible ` +
162
+ `(GET /api/v1/workflows/?slug=${templateSlug}).`);
146
163
  }
147
164
  // --- Build contextData + env ---
148
165
  const contextData = {
@@ -298,7 +315,8 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
298
315
  throw handleExternalServiceError(error, 'DebuggAI', 'crawl execution');
299
316
  }
300
317
  finally {
301
- process.stdin.removeListener('close', onStdinClose);
318
+ if (requestSignal)
319
+ requestSignal.removeEventListener('abort', onAbort);
302
320
  // Tunnel intentionally NOT torn down (reuse path per bead vwd).
303
321
  // If tunnel creation failed after key provision, revoke the orphaned key.
304
322
  if (!ctx.tunnelId && keyId) {
@@ -13,81 +13,43 @@ const POLL_INTERVAL_MAX_MS = 5000;
13
13
  const POLL_BACKOFF_MULTIPLIER = 1.5;
14
14
  const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
15
15
  /**
16
- * Stable dispatch slug for the App Evaluation workflow (bug clka). Dispatch
17
- * pins to this slug so a backend display-name change never breaks
18
- * check_app_in_browser. Backend contract sentinal-k8x1f.8 exposes the slug on
19
- * template results; until that deploy lands the resolver falls back to a
20
- * name-substring search (see EVAL_TEMPLATE_NAME_FALLBACK).
16
+ * Stable dispatch slugs for the three MCP browser workflows (bug clka /
17
+ * bead 56kd.8). All three handlers pin to a STABLE slug so a backend
18
+ * display-name change (or template rework) never breaks dispatch — the
19
+ * fuzzy name-substring fallback is fully retired now that the backend does
20
+ * server-side `?slug=` exact matching (contract sentinal-k8x1f.11).
21
21
  *
22
- * Override with the DEBUGGAI_EVAL_TEMPLATE env var to pin a different slug.
22
+ * Each is env-overridable to pin a different slug without a code change.
23
23
  */
24
- export const EVAL_TEMPLATE_SLUG = 'flow/e2es/app-eval';
25
- /** Interim name keyword used ONLY when the backend hasn't exposed slugs yet. */
26
- export const EVAL_TEMPLATE_NAME_FALLBACK = 'app evaluation workflow';
24
+ export const EVAL_TEMPLATE_SLUG = 'flow/e2es/app-eval'; // check_app_in_browser
25
+ export const PAGE_PROBE_TEMPLATE_SLUG = 'flow/tools/probe'; // probe_page
26
+ export const RAW_CRAWL_TEMPLATE_SLUG = 'crawl-execution-workflow-template'; // trigger_crawl
27
27
  /** The slug the eval-template dispatch pins to (env-overridable). */
28
28
  export function getEvalTemplateSlug() {
29
29
  return process.env.DEBUGGAI_EVAL_TEMPLATE || EVAL_TEMPLATE_SLUG;
30
30
  }
31
+ /** The slug the page-probe dispatch pins to (env-overridable). */
32
+ export function getPageProbeTemplateSlug() {
33
+ return process.env.DEBUGGAI_PROBE_TEMPLATE || PAGE_PROBE_TEMPLATE_SLUG;
34
+ }
35
+ /** The slug the raw-crawl dispatch pins to (env-overridable). */
36
+ export function getCrawlTemplateSlug() {
37
+ return process.env.DEBUGGAI_CRAWL_TEMPLATE || RAW_CRAWL_TEMPLATE_SLUG;
38
+ }
31
39
  export const createWorkflowsService = (tx) => {
32
40
  const service = {
33
- async findTemplateByName(keyword) {
34
- // Narrow server-side with `search` AND walk every page. The backend caps
35
- // the page size (it ignores page_size), so reading only page 1 silently
36
- // hides templates that sort later — that bug made check_app_in_browser
37
- // fail in prod because "App Evaluation Workflow Template" sat on page 2.
38
- // `search` collapses the candidate set to one page on backends that
39
- // support it; `page` paging is the fallback for those that ignore it.
40
- const needle = keyword.toLowerCase();
41
- const seenNames = [];
42
- const MAX_PAGES = 50; // safety valve against a backend that always returns `next`
43
- for (let page = 1; page <= MAX_PAGES; page++) {
44
- const response = await tx.get('api/v1/workflows/', { isTemplate: true, search: keyword, page });
45
- const templates = response?.results ?? [];
46
- for (const t of templates) {
47
- seenNames.push(t.name);
48
- if (t.name.toLowerCase().includes(needle))
49
- return t;
50
- }
51
- if (!response?.next)
52
- break;
53
- }
54
- if (seenNames.length === 0)
55
- return null;
56
- throw new Error(`No workflow template matching "${keyword}" found. ` +
57
- `Available templates: ${seenNames.map(n => `"${n}"`).join(', ')}. ` +
58
- `Ensure the backend has a template with "${keyword}" in its name.`);
59
- },
60
41
  async findTemplateBySlug(slug) {
61
- // Pin dispatch to the stable slug (bug clka). We pass `slug` as a query
62
- // param so a slug-aware backend can filter server-side; on backends that
63
- // ignore it we still walk the pages and match client-side on the `slug`
64
- // field. Returns null if NO result carries a slug field (backend hasn't
65
- // deployed the slug contract yet) the caller then falls back to name
66
- // search. `next`-paging mirrors findTemplateByName for the same reason
67
- // (the backend caps page size).
68
- const MAX_PAGES = 50;
69
- for (let page = 1; page <= MAX_PAGES; page++) {
70
- const response = await tx.get('api/v1/workflows/', { isTemplate: true, slug, page });
71
- const templates = response?.results ?? [];
72
- for (const t of templates) {
73
- if (t.slug === slug)
74
- return t;
75
- }
76
- if (!response?.next)
77
- break;
78
- }
79
- return null;
42
+ // Pure server-side slug resolve (bead 56kd.8). The backend does an EXACT
43
+ // `?slug=` match server-side (contract sentinal-k8x1f.11) and returns
44
+ // exactly that template, so this is a single GET NO page-walk, NO
45
+ // client-side name/slug filtering, NO name-substring fallback. A slug is
46
+ // the stable template identity, so a backend rename can never break us.
47
+ const response = await tx.get('api/v1/workflows/', { isTemplate: true, slug });
48
+ return response?.results?.[0] ?? null;
80
49
  },
81
50
  async findEvaluationTemplate() {
82
- // Primary: resolve by the stable slug so a backend rename can't break us.
83
- const slug = getEvalTemplateSlug();
84
- const bySlug = await service.findTemplateBySlug(slug);
85
- if (bySlug)
86
- return bySlug;
87
- // Fallback (interim, until backend contract sentinal-k8x1f.8 lands): the
88
- // name is specific enough to skip 'App Evaluation Brain' (subworkflow, no
89
- // browser lifecycle) which also contains 'app evaluation'.
90
- return service.findTemplateByName(EVAL_TEMPLATE_NAME_FALLBACK);
51
+ // Resolve the App Evaluation template purely by its stable slug.
52
+ return service.findTemplateBySlug(getEvalTemplateSlug());
91
53
  },
92
54
  async executeWorkflow(workflowUuid, contextData, env) {
93
55
  const body = { contextData };
@@ -86,6 +86,30 @@ export function buildTestPageChangesTool(ctx) {
86
86
  type: "string",
87
87
  description: "GitHub repository name (e.g. 'my-org/my-repo'). Auto-detected from the current git repo — only provide this if you want to run against a different project than the one you're in."
88
88
  },
89
+ auth: {
90
+ type: "object",
91
+ description: "Optional auth-precondition for a 'log in THEN deep-navigate' check. Set precondition:'login' to authenticate first (using the environment's credentials), then land on deepUrl. Use this instead of hoping the agent signs itself in at a login wall.",
92
+ properties: {
93
+ environmentId: {
94
+ type: "string",
95
+ description: "UUID of the environment whose credentials to log in with. See available environments in the tool description above."
96
+ },
97
+ precondition: {
98
+ type: "string",
99
+ enum: ["login", "none"],
100
+ description: "'login' = authenticate before evaluating; 'none' (default) = no login precondition."
101
+ },
102
+ entryUrl: {
103
+ type: "string",
104
+ description: "Optional URL of the login page to authenticate on."
105
+ },
106
+ deepUrl: {
107
+ type: "string",
108
+ description: "Optional URL to navigate to and evaluate AFTER login (e.g. a deep settings page). Falls back to `url` if omitted."
109
+ },
110
+ },
111
+ additionalProperties: false
112
+ },
89
113
  },
90
114
  required: ["description", "url"],
91
115
  additionalProperties: false
@@ -3,6 +3,22 @@
3
3
  */
4
4
  import { z } from 'zod';
5
5
  import { normalizeUrl } from '../utils/urlParser.js';
6
+ /**
7
+ * Auth-precondition deep-link block (bead debugg_ai_mcp-56kd.6).
8
+ *
9
+ * A first-class way to express "log in THEN deep-navigate to /settings/x" —
10
+ * forwarded verbatim into the dispatch contextData.auth per backend contract
11
+ * sentinal-k8x1f.8. The MCP is a THIN relay: it accepts the intent and forwards
12
+ * it; the backend authenticates (using the environment's credentials) and then
13
+ * navigates to deepUrl (falling back to the run's target_url). entryUrl/deepUrl
14
+ * are normalized the same way as the top-level url.
15
+ */
16
+ export const AuthPreconditionSchema = z.object({
17
+ environmentId: z.string().uuid().optional(),
18
+ precondition: z.enum(['login', 'none']).optional(),
19
+ entryUrl: z.preprocess(normalizeUrl, z.string().url('entryUrl must be a valid URL (the login page to authenticate on).').optional()),
20
+ deepUrl: z.preprocess(normalizeUrl, z.string().url('deepUrl must be a valid URL (the page to evaluate AFTER login).').optional()),
21
+ }).strict();
6
22
  /**
7
23
  * Tool input validation schemas
8
24
  */
@@ -16,6 +32,8 @@ export const TestPageChangesInputSchema = z.object({
16
32
  username: z.string().optional(),
17
33
  password: z.string().optional(),
18
34
  repoName: z.string().optional(),
35
+ // Auth-precondition deep-link intent (bead 56kd.6) — "log in THEN go to X".
36
+ auth: AuthPreconditionSchema.optional(),
19
37
  });
20
38
  export const TriggerCrawlInputSchema = z.object({
21
39
  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.')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {