@debugg-ai/debugg-ai-mcp 2.3.0 → 2.3.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.
@@ -14,10 +14,9 @@ import { detectRepoName } from '../utils/gitContext.js';
14
14
  import { tunnelManager } from '../services/ngrok/tunnelManager.js';
15
15
  import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js';
16
16
  import { extractLocalhostPort } from '../utils/urlParser.js';
17
+ import { getCachedTemplateUuid, getCachedProjectUuid, invalidateTemplateCache, invalidateProjectCache, } from '../utils/handlerCaches.js';
17
18
  const logger = new Logger({ module: 'testPageChangesHandler' });
18
- // Cache the template UUID and project UUIDs within a server session to avoid re-fetching
19
- let cachedTemplateUuid = null;
20
- const projectUuidCache = new Map();
19
+ const TEMPLATE_NAME = 'app evaluation';
21
20
  // Concurrency control — max 2 simultaneous browser checks.
22
21
  // Additional requests queue and run when a slot opens.
23
22
  const MAX_CONCURRENT = 2;
@@ -177,41 +176,33 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
177
176
  }
178
177
  }
179
178
  }
180
- // --- Find workflow template ---
179
+ // --- Resolve template + project in parallel (both independent post-tunnel) ---
181
180
  if (progressCallback) {
182
181
  await progressCallback({ progress: 2, total: TOTAL_STEPS, message: 'Locating evaluation workflow template...' });
183
182
  }
184
- if (!cachedTemplateUuid) {
185
- const template = await client.workflows.findEvaluationTemplate();
186
- if (!template) {
187
- throw new Error('App Evaluation Workflow Template not found. ' +
188
- 'Ensure the template is seeded in the backend (GET /api/v1/workflows/?is_template=true).');
189
- }
190
- cachedTemplateUuid = template.uuid;
191
- logger.info(`Using workflow template: ${template.name} (${template.uuid})`);
192
- }
193
- // --- Resolve project UUID (best-effort, non-blocking) ---
194
- // Use explicit repoName if provided, otherwise auto-detect from git remote
195
183
  const repoName = input.repoName || detectRepoName();
196
- let projectUuid;
197
- if (repoName) {
198
- projectUuid = projectUuidCache.get(repoName);
199
- if (!projectUuid) {
200
- try {
201
- const project = await client.findProjectByRepoName(repoName);
202
- if (project) {
203
- projectUuid = project.uuid;
204
- projectUuidCache.set(repoName, projectUuid);
205
- logger.info(`Resolved project: ${project.name} (${project.uuid})`);
184
+ const [templateUuid, projectUuid] = await Promise.all([
185
+ getCachedTemplateUuid(TEMPLATE_NAME, async () => {
186
+ return client.workflows.findEvaluationTemplate();
187
+ }),
188
+ repoName
189
+ ? getCachedProjectUuid(repoName, async (repo) => {
190
+ try {
191
+ return await client.findProjectByRepoName(repo);
206
192
  }
207
- else {
208
- logger.info(`No project found for repo "${repoName}" — proceeding without project_id`);
193
+ catch (err) {
194
+ logger.warn(`Failed to look up project for repo "${repo}": ${err}`);
195
+ return null;
209
196
  }
210
- }
211
- catch (err) {
212
- logger.warn(`Failed to look up project for repo "${repoName}": ${err}`);
213
- }
214
- }
197
+ })
198
+ : Promise.resolve(undefined),
199
+ ]);
200
+ if (!templateUuid) {
201
+ throw new Error('App Evaluation Workflow Template not found. ' +
202
+ 'Ensure the template is seeded in the backend (GET /api/v1/workflows/?is_template=true).');
203
+ }
204
+ if (repoName && !projectUuid) {
205
+ logger.info(`No project found for repo "${repoName}" — proceeding without project_id`);
215
206
  }
216
207
  // --- Build context data (camelCase here — axiosTransport auto-converts to snake_case) ---
217
208
  const contextData = {
@@ -238,7 +229,7 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
238
229
  if (progressCallback) {
239
230
  await progressCallback({ progress: 3, total: TOTAL_STEPS, message: 'Queuing workflow execution...' });
240
231
  }
241
- const executeResponse = await client.workflows.executeWorkflow(cachedTemplateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
232
+ const executeResponse = await client.workflows.executeWorkflow(templateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
242
233
  const executionUuid = executeResponse.executionUuid;
243
234
  logger.info(`Execution queued: ${executionUuid}`);
244
235
  // --- Poll ---
@@ -475,7 +466,8 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
475
466
  const duration = Date.now() - startTime;
476
467
  logger.toolError('check_app_in_browser', error, duration);
477
468
  if (error instanceof Error && (error.message.includes('not found') || error.message.includes('401'))) {
478
- cachedTemplateUuid = null;
469
+ invalidateTemplateCache();
470
+ invalidateProjectCache();
479
471
  }
480
472
  throw handleExternalServiceError(error, 'DebuggAI', 'test execution');
481
473
  }
@@ -19,6 +19,7 @@ import { tunnelManager } from '../services/ngrok/tunnelManager.js';
19
19
  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
+ import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
22
23
  const logger = new Logger({ module: 'triggerCrawlHandler' });
23
24
  const TEMPLATE_KEYWORD = 'raw crawl';
24
25
  export async function triggerCrawlHandler(input, context, rawProgressCallback) {
@@ -114,17 +115,17 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
114
115
  }
115
116
  }
116
117
  }
117
- // --- Find the crawl workflow template ---
118
+ // --- Find the crawl workflow template (cached across calls) ---
118
119
  if (progressCallback) {
119
120
  await progressCallback({ progress: 2, total: 4, message: 'Locating crawl workflow template...' });
120
121
  }
121
- const template = await client.workflows.findTemplateByName(TEMPLATE_KEYWORD);
122
- if (!template) {
122
+ const templateUuid = await getCachedTemplateUuid(TEMPLATE_KEYWORD, async (name) => {
123
+ return client.workflows.findTemplateByName(name);
124
+ });
125
+ if (!templateUuid) {
123
126
  throw new Error(`Raw Crawl Workflow Template not found. ` +
124
127
  `Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
125
128
  }
126
- const templateUuid = template.uuid;
127
- logger.info(`Using crawl template: ${template.name} (${templateUuid})`);
128
129
  // --- Build contextData + env ---
129
130
  const contextData = {
130
131
  targetUrl: ctx.targetUrl ?? ctx.originalUrl,
@@ -242,6 +243,9 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
242
243
  catch (error) {
243
244
  const duration = Date.now() - startTime;
244
245
  logger.toolError('trigger_crawl', error, duration);
246
+ if (error instanceof Error && (error.message.includes('not found') || error.message.includes('401'))) {
247
+ invalidateTemplateCache();
248
+ }
245
249
  throw handleExternalServiceError(error, 'DebuggAI', 'crawl execution');
246
250
  }
247
251
  finally {
@@ -4,7 +4,13 @@
4
4
  */
5
5
  import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
6
6
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
7
- const POLL_INTERVAL_MS = 3000;
7
+ // Exponential backoff polling: short executions (10-15s crawls) detect terminal
8
+ // status quickly via the early polls; long executions (60-150s browser runs)
9
+ // avoid hammering the backend with 20+ roundtrips. Cap at 5s so we never wait
10
+ // more than 5s past terminal-state achievement.
11
+ const POLL_INTERVAL_INITIAL_MS = 1000;
12
+ const POLL_INTERVAL_MAX_MS = 5000;
13
+ const POLL_BACKOFF_MULTIPLIER = 1.5;
8
14
  const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
9
15
  export const createWorkflowsService = (tx) => {
10
16
  const service = {
@@ -82,6 +88,7 @@ export const createWorkflowsService = (tx) => {
82
88
  const deadline = Date.now() + EXECUTION_TIMEOUT_MS;
83
89
  const pollStart = Date.now();
84
90
  let pollCount = 0;
91
+ let intervalMs = POLL_INTERVAL_INITIAL_MS;
85
92
  while (Date.now() < deadline) {
86
93
  if (signal?.aborted) {
87
94
  throw new Error(`Polling cancelled for execution ${executionUuid}`);
@@ -99,6 +106,7 @@ export const createWorkflowsService = (tx) => {
99
106
  stepsTaken: execution.state?.stepsTaken ?? 0,
100
107
  durationMs: Date.now() - pollStart,
101
108
  pollCount,
109
+ finalIntervalMs: intervalMs,
102
110
  });
103
111
  return execution;
104
112
  }
@@ -106,8 +114,9 @@ export const createWorkflowsService = (tx) => {
106
114
  if (signal?.aborted) {
107
115
  throw new Error(`Polling cancelled for execution ${executionUuid}`);
108
116
  }
117
+ const sleepMs = intervalMs;
109
118
  await new Promise((resolve, reject) => {
110
- const timer = setTimeout(resolve, POLL_INTERVAL_MS);
119
+ const timer = setTimeout(resolve, sleepMs);
111
120
  if (signal) {
112
121
  const onAbort = () => { clearTimeout(timer); reject(new Error(`Polling cancelled for execution ${executionUuid}`)); };
113
122
  if (signal.aborted) {
@@ -118,6 +127,9 @@ export const createWorkflowsService = (tx) => {
118
127
  signal.addEventListener('abort', onAbort, { once: true });
119
128
  }
120
129
  });
130
+ // Backoff for next iteration — capped at MAX so we don't wait too long
131
+ // past terminal-state achievement on the longest runs.
132
+ intervalMs = Math.min(Math.round(intervalMs * POLL_BACKOFF_MULTIPLIER), POLL_INTERVAL_MAX_MS);
121
133
  }
122
134
  throw new Error(`Execution ${executionUuid} timed out after ${EXECUTION_TIMEOUT_MS / 1000}s`);
123
135
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared, process-scoped caches for handler hot-path lookups.
3
+ *
4
+ * Replaces per-handler `let cachedX: string | null = null` singletons with
5
+ * a single source of truth. Two handlers (testPageChanges + triggerCrawl)
6
+ * resolve the same workflow templates and the same project UUIDs; sharing
7
+ * the cache avoids duplicate backend roundtrips when the user invokes both.
8
+ *
9
+ * Each lookup emits a telemetry event with `hit: boolean` so the cache
10
+ * effectiveness is observable in PostHog without log-grepping.
11
+ *
12
+ * Caches are NEVER persisted across processes. Cleared explicitly by the
13
+ * handlers on auth failures (so a rotated API key doesn't keep serving
14
+ * stale UUIDs).
15
+ */
16
+ import { Telemetry, TelemetryEvents } from './telemetry.js';
17
+ const templateUuidByName = new Map();
18
+ const projectUuidByRepo = new Map();
19
+ /**
20
+ * Get a workflow template UUID by name, populating the cache on miss.
21
+ * Returns null if the lookup function returns null (template doesn't exist).
22
+ */
23
+ export async function getCachedTemplateUuid(name, lookup) {
24
+ const cached = templateUuidByName.get(name);
25
+ if (cached) {
26
+ Telemetry.capture(TelemetryEvents.TEMPLATE_LOOKUP, { templateName: name, hit: true });
27
+ return cached;
28
+ }
29
+ const t0 = Date.now();
30
+ const template = await lookup(name);
31
+ Telemetry.capture(TelemetryEvents.TEMPLATE_LOOKUP, {
32
+ templateName: name,
33
+ hit: false,
34
+ durationMs: Date.now() - t0,
35
+ found: !!template,
36
+ });
37
+ if (!template)
38
+ return null;
39
+ templateUuidByName.set(name, template.uuid);
40
+ return template.uuid;
41
+ }
42
+ /**
43
+ * Get a project UUID by repo name, populating the cache on miss.
44
+ * Returns undefined if the lookup function returns null (no matching project).
45
+ */
46
+ export async function getCachedProjectUuid(repoName, lookup) {
47
+ const cached = projectUuidByRepo.get(repoName);
48
+ if (cached) {
49
+ Telemetry.capture(TelemetryEvents.PROJECT_LOOKUP, { repoName, hit: true });
50
+ return cached;
51
+ }
52
+ const t0 = Date.now();
53
+ const project = await lookup(repoName);
54
+ Telemetry.capture(TelemetryEvents.PROJECT_LOOKUP, {
55
+ repoName,
56
+ hit: false,
57
+ durationMs: Date.now() - t0,
58
+ found: !!project,
59
+ });
60
+ if (!project)
61
+ return undefined;
62
+ projectUuidByRepo.set(repoName, project.uuid);
63
+ return project.uuid;
64
+ }
65
+ /**
66
+ * Clear the template cache. Called by handlers on auth failures.
67
+ */
68
+ export function invalidateTemplateCache() {
69
+ templateUuidByName.clear();
70
+ }
71
+ /**
72
+ * Clear the project cache. Same trigger as templates.
73
+ */
74
+ export function invalidateProjectCache() {
75
+ projectUuidByRepo.clear();
76
+ }
@@ -55,4 +55,6 @@ export const TelemetryEvents = {
55
55
  TUNNEL_PROVISIONED: 'tunnel.provisioned',
56
56
  TUNNEL_PROVISION_RETRY: 'tunnel.provision_retry',
57
57
  TUNNEL_STOPPED: 'tunnel.stopped',
58
+ TEMPLATE_LOOKUP: 'template.lookup',
59
+ PROJECT_LOOKUP: 'project.lookup',
58
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.3.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": {