@debugg-ai/debugg-ai-mcp 1.0.28 → 1.0.30

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.
@@ -1,6 +1 @@
1
- /**
2
- * Handler exports for DebuggAI MCP Server
3
- */
4
1
  export * from './testPageChangesHandler.js';
5
- export * from './e2eSuiteHandlers.js';
6
- export * from './liveSessionHandlers.js';
@@ -8,38 +8,19 @@ import { Logger } from '../utils/logger.js';
8
8
  import { handleExternalServiceError } from '../utils/errors.js';
9
9
  import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
10
10
  import { DebuggAIServerClient } from '../services/index.js';
11
- import { tunnelManager } from '../services/ngrok/tunnelManager.js';
12
- import { extractLocalhostPort, replaceTunnelUrls } from '../utils/urlParser.js';
11
+ import { resolveTargetUrl, buildContext, ensureTunnel, releaseTunnel, sanitizeResponseUrls, } from '../utils/tunnelContext.js';
13
12
  const logger = new Logger({ module: 'testPageChangesHandler' });
14
13
  // Cache the template UUID within a server session to avoid re-fetching
15
14
  let cachedTemplateUuid = null;
16
- function isLocalhostUrl(url) {
17
- return url.includes('localhost') || url.includes('127.0.0.1');
18
- }
19
- /**
20
- * Resolve the target URL from input.
21
- * - If `url` is provided, use it directly.
22
- * - If only `localPort` is provided, construct http://localhost:{port}.
23
- * - Otherwise error.
24
- */
25
- function resolveTargetUrl(input) {
26
- if (input.url)
27
- return input.url;
28
- if (input.localPort)
29
- return `http://localhost:${input.localPort}`;
30
- throw new Error('Provide a target URL via the "url" parameter (e.g. "https://example.com") ' +
31
- 'or a "localPort" for a local dev server.');
32
- }
33
15
  export async function testPageChangesHandler(input, context, progressCallback) {
34
16
  const startTime = Date.now();
35
17
  logger.toolStart('check_app_in_browser', input);
36
18
  const client = new DebuggAIServerClient(config.api.key);
37
19
  await client.init();
38
- let tunnelId;
20
+ const originalUrl = resolveTargetUrl(input);
21
+ let ctx = buildContext(originalUrl);
39
22
  let ngrokKeyId;
40
23
  try {
41
- const targetUrlRaw = resolveTargetUrl(input);
42
- const isLocalhost = isLocalhostUrl(targetUrlRaw);
43
24
  // --- Find workflow template ---
44
25
  if (progressCallback) {
45
26
  await progressCallback({ progress: 1, total: 10, message: 'Locating evaluation workflow template...' });
@@ -55,7 +36,7 @@ export async function testPageChangesHandler(input, context, progressCallback) {
55
36
  }
56
37
  // --- Build context data ---
57
38
  const contextData = {
58
- targetUrl: targetUrlRaw,
39
+ targetUrl: originalUrl,
59
40
  goal: input.description,
60
41
  };
61
42
  // --- Build env (credentials/environment) ---
@@ -78,37 +59,39 @@ export async function testPageChangesHandler(input, context, progressCallback) {
78
59
  const executionUuid = executeResponse.executionUuid;
79
60
  ngrokKeyId = executeResponse.ngrokKeyId ?? undefined;
80
61
  logger.info(`Execution queued: ${executionUuid}`);
81
- // --- Localhost tunneling (after execute, using tunnel_key + executionUuid as subdomain) ---
82
- if (isLocalhost) {
62
+ // --- Tunnel (after execute backend returns tunnelKey, executionUuid is the subdomain) ---
63
+ if (ctx.isLocalhost) {
83
64
  if (progressCallback) {
84
65
  await progressCallback({ progress: 3, total: 10, message: 'Creating secure tunnel for localhost...' });
85
66
  }
86
- const port = extractLocalhostPort(targetUrlRaw);
87
- if (!port) {
88
- throw new Error(`Could not extract port from localhost URL: ${targetUrlRaw}`);
89
- }
90
67
  if (!executeResponse.tunnelKey) {
91
68
  throw new Error('Backend did not return a tunnel key for localhost execution');
92
69
  }
93
- tunnelId = executionUuid;
94
- const tunnelResult = await tunnelManager.processUrl(targetUrlRaw, executeResponse.tunnelKey, tunnelId);
95
- logger.info(`Tunnel ready: ${tunnelResult.url}`);
70
+ ctx = await ensureTunnel(ctx, executeResponse.tunnelKey, executionUuid);
71
+ logger.info(`Tunnel ready for ${originalUrl} (id: ${executionUuid})`);
96
72
  }
97
73
  // --- Poll ---
98
- let lastSteps = 0;
74
+ // nodeExecutions grows as each node completes: trigger → browser.setup → surfer.execute_task → browser.teardown
75
+ const NODE_PHASE_LABELS = {
76
+ 0: 'Browser agent starting up...',
77
+ 1: 'Browser ready, agent navigating...',
78
+ 2: 'Agent evaluating app...',
79
+ 3: 'Wrapping up...',
80
+ };
81
+ let lastNodeCount = 0;
99
82
  const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
100
- const steps = exec.state?.stepsTaken ?? 0;
101
- if (steps !== lastSteps || exec.status !== 'pending') {
102
- lastSteps = steps;
103
- logger.info(`Execution status: ${exec.status}, steps: ${steps}`);
83
+ const nodeCount = exec.nodeExecutions?.length ?? 0;
84
+ if (nodeCount !== lastNodeCount || exec.status !== 'pending') {
85
+ lastNodeCount = nodeCount;
86
+ logger.info(`Execution status: ${exec.status}, nodes completed: ${nodeCount}`);
104
87
  }
105
88
  if (progressCallback) {
106
- const progress = Math.min(3 + steps, 9);
107
- await progressCallback({
108
- progress,
109
- total: 10,
110
- message: `${exec.status}: ${steps} step${steps !== 1 ? 's' : ''} taken`,
111
- }).catch(() => { });
89
+ // Map 0-4 completed nodes to progress 3-9 (3 reserved for tunnel setup)
90
+ const progress = Math.min(3 + nodeCount * 2, 9);
91
+ const message = exec.status === 'running'
92
+ ? (NODE_PHASE_LABELS[nodeCount] ?? 'Agent working...')
93
+ : exec.status;
94
+ await progressCallback({ progress, total: 10, message });
112
95
  }
113
96
  });
114
97
  const duration = Date.now() - startTime;
@@ -120,30 +103,22 @@ export async function testPageChangesHandler(input, context, progressCallback) {
120
103
  success: finalExecution.state?.success ?? false,
121
104
  status: finalExecution.status,
122
105
  stepsTaken: finalExecution.state?.stepsTaken ?? surferNode?.outputData?.stepsTaken ?? 0,
123
- targetUrl: targetUrlRaw,
106
+ targetUrl: originalUrl,
124
107
  executionId: executionUuid,
125
108
  durationMs: finalExecution.durationMs ?? duration,
126
109
  };
127
- if (finalExecution.state?.error) {
110
+ if (finalExecution.state?.error)
128
111
  responsePayload.agentError = finalExecution.state.error;
129
- }
130
- if (finalExecution.errorMessage) {
112
+ if (finalExecution.errorMessage)
131
113
  responsePayload.errorMessage = finalExecution.errorMessage;
132
- }
133
- if (finalExecution.errorInfo?.failedNodeId) {
114
+ if (finalExecution.errorInfo?.failedNodeId)
134
115
  responsePayload.failedNode = finalExecution.errorInfo.failedNodeId;
135
- }
136
- if (executeResponse.resolvedEnvironmentId) {
116
+ if (executeResponse.resolvedEnvironmentId)
137
117
  responsePayload.resolvedEnvironmentId = executeResponse.resolvedEnvironmentId;
138
- }
139
- if (executeResponse.resolvedCredentialId) {
118
+ if (executeResponse.resolvedCredentialId)
140
119
  responsePayload.resolvedCredentialId = executeResponse.resolvedCredentialId;
141
- }
142
120
  if (surferNode?.outputData) {
143
- const surferOutput = isLocalhost
144
- ? replaceTunnelUrls(surferNode.outputData, new URL(targetUrlRaw).origin)
145
- : surferNode.outputData;
146
- responsePayload.surferOutput = surferOutput;
121
+ responsePayload.surferOutput = sanitizeResponseUrls(surferNode.outputData, ctx);
147
122
  }
148
123
  logger.toolComplete('check_app_in_browser', duration);
149
124
  if (progressCallback) {
@@ -171,20 +146,15 @@ export async function testPageChangesHandler(input, context, progressCallback) {
171
146
  catch (error) {
172
147
  const duration = Date.now() - startTime;
173
148
  logger.toolError('check_app_in_browser', error, duration);
174
- // Invalidate cached template UUID on auth/not-found errors
175
149
  if (error instanceof Error && (error.message.includes('not found') || error.message.includes('401'))) {
176
150
  cachedTemplateUuid = null;
177
151
  }
178
152
  throw handleExternalServiceError(error, 'DebuggAI', 'test execution');
179
153
  }
180
154
  finally {
181
- // Revoke the short-lived ngrok key
182
155
  if (ngrokKeyId) {
183
156
  client.revokeNgrokKey(ngrokKeyId).catch(err => logger.warn(`Failed to revoke ngrok key ${ngrokKeyId}: ${err}`));
184
157
  }
185
- // Clean up tunnel if we created one
186
- if (tunnelId) {
187
- tunnelManager.stopTunnel(tunnelId).catch(err => logger.warn(`Failed to stop tunnel ${tunnelId}: ${err}`));
188
- }
158
+ releaseTunnel(ctx).catch(err => logger.warn(`Failed to stop tunnel: ${err}`));
189
159
  }
190
160
  }
@@ -1,5 +1,3 @@
1
- import { createE2esService } from "./e2es.js";
2
- import { createBrowserSessionsService } from "./browserSessions.js";
3
1
  import { createWorkflowsService } from "./workflows.js";
4
2
  import { AxiosTransport } from "../utils/axiosTransport.js";
5
3
  import { config } from "../config/index.js";
@@ -34,9 +32,6 @@ export class DebuggAIServerClient {
34
32
  userApiKey;
35
33
  tx;
36
34
  url;
37
- // Public "sub‑APIs"
38
- e2es;
39
- browserSessions;
40
35
  workflows;
41
36
  constructor(userApiKey) {
42
37
  this.userApiKey = userApiKey;
@@ -46,8 +41,6 @@ export class DebuggAIServerClient {
46
41
  const serverUrl = config.api.baseUrl;
47
42
  this.url = new URL(serverUrl);
48
43
  this.tx = new DebuggTransport({ baseUrl: serverUrl, apiKey: this.userApiKey, tokenType: config.api.tokenType });
49
- this.e2es = createE2esService(this.tx);
50
- this.browserSessions = createBrowserSessionsService(this.tx);
51
44
  this.workflows = createWorkflowsService(this.tx);
52
45
  }
53
46
  /**
@@ -1,61 +1,17 @@
1
- /**
2
- * Tool registry and exports
3
- * Centralized location for all DebuggAI MCP tool definitions
4
- *
5
- * These tools provide AI agents with:
6
- * - Live remote browser sessions for real-time monitoring
7
- * - End-to-end testing with natural language descriptions
8
- * - Browser console logs, network traffic, and screenshot capture
9
- * - Test suite management and Git commit-based test generation
10
- */
11
1
  import { testPageChangesTool, validatedTestPageChangesTool } from './testPageChanges.js';
12
- import { startLiveSessionTool, stopLiveSessionTool, getLiveSessionStatusTool, getLiveSessionLogsTool, getLiveSessionScreenshotTool, validatedLiveSessionTools } from './liveSession.js';
13
- import { listTestsTool, listTestSuitesTool, createTestSuiteTool, createCommitSuiteTool, listCommitSuitesTool, getTestStatusTool, validatedE2ESuiteTools } from './e2eSuites.js';
14
- import { quickScreenshotTool, validatedQuickScreenshotTool } from './quickScreenshot.js';
15
- /**
16
- * All available tools for MCP server
17
- */
18
2
  export const tools = [
19
3
  testPageChangesTool,
20
- startLiveSessionTool,
21
- stopLiveSessionTool,
22
- getLiveSessionStatusTool,
23
- getLiveSessionLogsTool,
24
- getLiveSessionScreenshotTool,
25
- listTestsTool,
26
- listTestSuitesTool,
27
- createTestSuiteTool,
28
- createCommitSuiteTool,
29
- listCommitSuitesTool,
30
- getTestStatusTool,
31
- quickScreenshotTool,
32
4
  ];
33
- /**
34
- * All validated tools with handlers
35
- */
36
5
  export const validatedTools = [
37
6
  validatedTestPageChangesTool,
38
- ...validatedLiveSessionTools,
39
- ...validatedE2ESuiteTools,
40
- validatedQuickScreenshotTool,
41
7
  ];
42
- /**
43
- * Tool registry for quick lookup
44
- */
45
8
  export const toolRegistry = new Map();
46
- // Initialize tool registry
47
9
  for (const tool of validatedTools) {
48
10
  toolRegistry.set(tool.name, tool);
49
11
  }
50
- /**
51
- * Get tool by name
52
- */
53
12
  export function getTool(name) {
54
13
  return toolRegistry.get(name);
55
14
  }
56
- /**
57
- * Check if tool exists
58
- */
59
15
  export function hasToolTool(name) {
60
16
  return toolRegistry.has(name);
61
17
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared tunnel and URL resolution context used by all MCP tools.
3
+ *
4
+ * Centralizes:
5
+ * - resolving user input (url / localPort) to a concrete URL
6
+ * - creating / reusing ngrok tunnels after the backend returns a tunnelKey
7
+ * - sanitizing backend responses so callers only ever see the original URL
8
+ */
9
+ import { tunnelManager } from '../services/ngrok/tunnelManager.js';
10
+ import { isLocalhostUrl, replaceTunnelUrls } from './urlParser.js';
11
+ // ─── URL resolution ──────────────────────────────────────────────────────────
12
+ /**
13
+ * Resolve tool input to a concrete URL string.
14
+ * Accepts either a `url` string or a `localPort` number; throws if neither provided.
15
+ */
16
+ export function resolveTargetUrl(input) {
17
+ if (input.url)
18
+ return input.url;
19
+ if (input.localPort)
20
+ return `http://localhost:${input.localPort}`;
21
+ throw new Error('Provide a target URL via "url" (e.g. "https://example.com") ' +
22
+ 'or "localPort" for a local dev server.');
23
+ }
24
+ /**
25
+ * Build a TunnelContext for a resolved URL.
26
+ * Call this right after resolving the target URL — before any backend call.
27
+ */
28
+ export function buildContext(originalUrl) {
29
+ return {
30
+ originalUrl,
31
+ isLocalhost: isLocalhostUrl(originalUrl),
32
+ };
33
+ }
34
+ // ─── Tunnel creation ─────────────────────────────────────────────────────────
35
+ /**
36
+ * Create (or reuse) a tunnel for a localhost URL.
37
+ *
38
+ * Call this AFTER the backend returns a `tunnelKey` and `tunnelId`
39
+ * (e.g. executionUuid from executeWorkflow, sessionId from startSession).
40
+ *
41
+ * No-op and returns null for public URLs.
42
+ *
43
+ * @param ctx - Context built from `buildContext()`
44
+ * @param tunnelKey - Auth token from the backend (short-lived ngrok key)
45
+ * @param tunnelId - ID to use as the ngrok subdomain (must match what the backend expects)
46
+ */
47
+ export async function ensureTunnel(ctx, tunnelKey, tunnelId) {
48
+ if (!ctx.isLocalhost)
49
+ return ctx;
50
+ const result = await tunnelManager.processUrl(ctx.originalUrl, tunnelKey, tunnelId);
51
+ return { ...ctx, tunnelId: result.tunnelId };
52
+ }
53
+ /**
54
+ * Stop the tunnel associated with a context (fire-and-forget safe).
55
+ */
56
+ export async function releaseTunnel(ctx) {
57
+ if (ctx.tunnelId) {
58
+ await tunnelManager.stopTunnel(ctx.tunnelId);
59
+ }
60
+ }
61
+ // ─── Response sanitization ───────────────────────────────────────────────────
62
+ /**
63
+ * Replace any tunnel URLs in a backend response with the original localhost origin.
64
+ * No-op when the original URL was not localhost.
65
+ *
66
+ * Handles nested objects, arrays, and strings recursively.
67
+ */
68
+ export function sanitizeResponseUrls(value, ctx) {
69
+ if (!ctx.isLocalhost)
70
+ return value;
71
+ const origin = new URL(ctx.originalUrl).origin;
72
+ return replaceTunnelUrls(value, origin);
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,8 @@
18
18
  "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
19
  "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.integration.config.js",
20
20
  "test:integration:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.integration.config.js --watch",
21
+ "test:e2e": "node scripts/e2e.mjs",
22
+ "test:e2e:skip-build": "node scripts/e2e.mjs --skip-build",
21
23
  "version:patch": "npm version patch --no-git-tag-version",
22
24
  "version:minor": "npm version minor --no-git-tag-version",
23
25
  "version:major": "npm version major --no-git-tag-version",