@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.
- package/dist/handlers/index.js +0 -5
- package/dist/handlers/testPageChangesHandler.js +34 -64
- package/dist/services/index.js +0 -7
- package/dist/tools/index.js +0 -44
- package/dist/utils/tunnelContext.js +73 -0
- package/package.json +3 -1
- package/dist/handlers/e2eSuiteHandlers.js +0 -526
- package/dist/handlers/liveSessionHandlers.js +0 -373
- package/dist/services/browserSessions.js +0 -282
- package/dist/services/e2es.js +0 -290
- package/dist/tools/e2eSuites.js +0 -259
- package/dist/tools/liveSession.js +0 -217
- package/dist/tools/quickScreenshot.js +0 -245
package/dist/handlers/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
// ---
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
logger.info(`Execution status: ${exec.status},
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/services/index.js
CHANGED
|
@@ -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
|
/**
|
package/dist/tools/index.js
CHANGED
|
@@ -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.
|
|
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",
|