@debugg-ai/debugg-ai-mcp 1.0.30 → 1.0.32
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 +21 -108
- package/dist/handlers/testPageChangesHandler.js +75 -26
- package/dist/services/index.js +3 -0
- package/dist/services/ngrok/tunnelManager.js +250 -225
- package/dist/services/ngrok/tunnelRegistry.js +75 -0
- package/dist/services/tunnels.js +19 -0
- package/dist/services/workflows.js +21 -6
- package/dist/tools/testPageChanges.js +2 -8
- package/dist/types/index.js +2 -74
- package/dist/utils/tunnelContext.js +30 -17
- package/dist/utils/urlParser.js +29 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Debugg AI — MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI-powered browser testing via the [Model Context Protocol](https://modelcontextprotocol.io). Point it at any URL (or localhost) and describe what to test — an AI agent browses your app and returns pass/fail with screenshots.
|
|
4
4
|
|
|
5
5
|
<a href="https://glama.ai/mcp/servers/@debugg-ai/debugg-ai-mcp">
|
|
6
6
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/@debugg-ai/debugg-ai-mcp/badge" alt="Debugg AI MCP server" />
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## What it does
|
|
12
|
-
|
|
13
|
-
- **Run browser tests with natural language** — describe what to test, the AI agent clicks through your app and returns screenshots + results
|
|
14
|
-
- **Monitor live browser sessions** — capture console logs, network requests, and screenshots in real time
|
|
15
|
-
- **Manage test suites** — create, organize, and track E2E tests tied to features or commits
|
|
16
|
-
- **Seamless CI/CD** — view all results in your [Debugg.AI dashboard](https://app.debugg.ai)
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Demo
|
|
21
|
-
|
|
22
|
-
### Prompt: "Test the ability to create an account and login"
|
|
23
|
-
|
|
24
|
-

|
|
25
|
-
|
|
26
|
-
**Result:**
|
|
27
|
-
- Duration: 86.80 seconds
|
|
28
|
-
- Status: Success — signed up and logged in with `alice.wonderland1234@example.com`
|
|
9
|
+
## Setup
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Quick Setup
|
|
35
|
-
|
|
36
|
-
### 1. Get your API key
|
|
37
|
-
Create a free account at [debugg.ai](https://debugg.ai) and generate your API key.
|
|
38
|
-
|
|
39
|
-
### 2. Add to Claude Desktop
|
|
11
|
+
Get an API key at [debugg.ai](https://debugg.ai), then add to your MCP client config:
|
|
40
12
|
|
|
41
13
|
```json
|
|
42
14
|
{
|
|
@@ -52,101 +24,42 @@ Create a free account at [debugg.ai](https://debugg.ai) and generate your API ke
|
|
|
52
24
|
}
|
|
53
25
|
```
|
|
54
26
|
|
|
55
|
-
|
|
27
|
+
Or with Docker:
|
|
28
|
+
|
|
56
29
|
```bash
|
|
57
|
-
docker run -i --rm --init
|
|
58
|
-
-e DEBUGGAI_API_KEY=your_api_key \
|
|
59
|
-
quinnosha/debugg-ai-mcp
|
|
30
|
+
docker run -i --rm --init -e DEBUGGAI_API_KEY=your_api_key quinnosha/debugg-ai-mcp
|
|
60
31
|
```
|
|
61
32
|
|
|
62
|
-
|
|
33
|
+
## `check_app_in_browser`
|
|
63
34
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
### E2E Testing
|
|
67
|
-
| Tool | Description |
|
|
68
|
-
|------|-------------|
|
|
69
|
-
| `check_app_in_browser` | Run a browser test with a natural language description. Returns screenshots and pass/fail result. |
|
|
70
|
-
| `create_test_suite` | Generate a suite of browser tests for a feature or workflow |
|
|
71
|
-
| `create_commit_suite` | Auto-generate tests from recent git commits |
|
|
72
|
-
| `get_test_status` | Check progress and results of a running or completed test suite |
|
|
73
|
-
|
|
74
|
-
### Test Management
|
|
75
|
-
| Tool | Description |
|
|
76
|
-
|------|-------------|
|
|
77
|
-
| `list_tests` | List all E2E tests with filtering and pagination |
|
|
78
|
-
| `list_test_suites` | List all test suites |
|
|
79
|
-
| `list_commit_suites` | List all commit-based test suites |
|
|
80
|
-
|
|
81
|
-
### Live Session Monitoring
|
|
82
|
-
| Tool | Description |
|
|
83
|
-
|------|-------------|
|
|
84
|
-
| `start_live_session` | Launch a remote browser session with real-time monitoring |
|
|
85
|
-
| `stop_live_session` | Stop an active session and save captured data |
|
|
86
|
-
| `get_live_session_status` | Check session status, current URL, and uptime |
|
|
87
|
-
| `get_live_session_logs` | Retrieve console logs, network requests, and JS errors |
|
|
88
|
-
| `get_live_session_screenshot` | Capture a screenshot of what the browser currently shows |
|
|
89
|
-
|
|
90
|
-
### Quick Operations
|
|
91
|
-
| Tool | Description |
|
|
92
|
-
|------|-------------|
|
|
93
|
-
| `quick_screenshot` | Capture a screenshot of any URL — no session setup required |
|
|
35
|
+
Runs an AI browser agent against your app. The agent navigates, interacts, and reports back with screenshots.
|
|
94
36
|
|
|
95
|
-
|
|
37
|
+
| Parameter | Type | Description |
|
|
38
|
+
|-----------|------|-------------|
|
|
39
|
+
| `description` | string **required** | What to test (natural language) |
|
|
40
|
+
| `url` | string | Target URL — required if `localPort` not set |
|
|
41
|
+
| `localPort` | number | Local dev server port — tunnel created automatically |
|
|
42
|
+
| `environmentId` | string | UUID of a specific environment |
|
|
43
|
+
| `credentialId` | string | UUID of a specific credential |
|
|
44
|
+
| `credentialRole` | string | Pick a credential by role (e.g. `admin`, `guest`) |
|
|
45
|
+
| `username` | string | Username for login |
|
|
46
|
+
| `password` | string | Password for login |
|
|
96
47
|
|
|
97
48
|
## Configuration
|
|
98
49
|
|
|
99
50
|
```bash
|
|
100
|
-
# Required
|
|
101
51
|
DEBUGGAI_API_KEY=your_api_key
|
|
102
|
-
|
|
103
|
-
# Optional — provide defaults so you don't have to pass them every time
|
|
104
|
-
DEBUGGAI_LOCAL_PORT=3000 # Your app's local port
|
|
105
|
-
DEBUGGAI_LOCAL_REPO_NAME=your-org/repo # GitHub repo name
|
|
106
|
-
DEBUGGAI_LOCAL_REPO_PATH=/path/to/project # Absolute path to project root
|
|
107
|
-
DEBUGGAI_LOCAL_BRANCH_NAME=main # Current branch
|
|
108
|
-
|
|
109
|
-
# Override API endpoint (defaults to https://api.debugg.ai)
|
|
110
|
-
DEBUGGAI_API_URL=https://api.debugg.ai
|
|
111
52
|
```
|
|
112
53
|
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Usage examples
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
"Test the user login flow on my app running on port 3000"
|
|
119
|
-
|
|
120
|
-
"Check that the checkout process works end to end"
|
|
121
|
-
|
|
122
|
-
"Take a screenshot of localhost:3000 and tell me if anything looks broken"
|
|
123
|
-
|
|
124
|
-
"Create a test suite for the user authentication feature"
|
|
125
|
-
|
|
126
|
-
"Generate browser tests for my last 3 commits"
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
54
|
## Local Development
|
|
132
55
|
|
|
133
56
|
```bash
|
|
134
|
-
npm install
|
|
135
|
-
npm test
|
|
136
|
-
npm run build
|
|
137
|
-
|
|
138
|
-
# Test with MCP inspector
|
|
139
|
-
npx @modelcontextprotocol/inspector --config test-config.json --server debugg-ai
|
|
57
|
+
npm install && npm test && npm run build
|
|
140
58
|
```
|
|
141
59
|
|
|
142
|
-
---
|
|
143
|
-
|
|
144
60
|
## Links
|
|
145
61
|
|
|
146
|
-
|
|
147
|
-
- **Docs**: [debugg.ai/docs](https://debugg.ai/docs)
|
|
148
|
-
- **Issues**: [GitHub Issues](https://github.com/debugg-ai/debugg-ai-mcp/issues)
|
|
149
|
-
- **Discord**: [debugg.ai/discord](https://debugg.ai/discord)
|
|
62
|
+
[Dashboard](https://app.debugg.ai) · [Docs](https://debugg.ai/docs) · [Issues](https://github.com/debugg-ai/debugg-ai-mcp/issues) · [Discord](https://debugg.ai/discord)
|
|
150
63
|
|
|
151
64
|
---
|
|
152
65
|
|
|
@@ -8,7 +8,7 @@ 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 { resolveTargetUrl, buildContext,
|
|
11
|
+
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, } from '../utils/tunnelContext.js';
|
|
12
12
|
const logger = new Logger({ module: 'testPageChangesHandler' });
|
|
13
13
|
// Cache the template UUID within a server session to avoid re-fetching
|
|
14
14
|
let cachedTemplateUuid = null;
|
|
@@ -19,11 +19,32 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
19
19
|
await client.init();
|
|
20
20
|
const originalUrl = resolveTargetUrl(input);
|
|
21
21
|
let ctx = buildContext(originalUrl);
|
|
22
|
-
let
|
|
22
|
+
let keyId;
|
|
23
|
+
const abortController = new AbortController();
|
|
24
|
+
const onStdinClose = () => abortController.abort();
|
|
25
|
+
process.stdin.once('close', onStdinClose);
|
|
23
26
|
try {
|
|
27
|
+
// --- Tunnel: reuse existing or provision a fresh one ---
|
|
28
|
+
if (ctx.isLocalhost) {
|
|
29
|
+
if (progressCallback) {
|
|
30
|
+
await progressCallback({ progress: 1, total: 10, message: 'Provisioning secure tunnel for localhost...' });
|
|
31
|
+
}
|
|
32
|
+
const reused = findExistingTunnel(ctx);
|
|
33
|
+
if (reused) {
|
|
34
|
+
ctx = reused;
|
|
35
|
+
logger.info(`Reusing tunnel: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const tunnel = await client.tunnels.provision();
|
|
39
|
+
keyId = tunnel.keyId;
|
|
40
|
+
// revokeKey is stored on the TunnelInfo and fires when the tunnel auto-stops.
|
|
41
|
+
ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
|
|
42
|
+
logger.info(`Tunnel ready: ${ctx.targetUrl} (id: ${ctx.tunnelId})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
24
45
|
// --- Find workflow template ---
|
|
25
46
|
if (progressCallback) {
|
|
26
|
-
await progressCallback({ progress:
|
|
47
|
+
await progressCallback({ progress: 2, total: 10, message: 'Locating evaluation workflow template...' });
|
|
27
48
|
}
|
|
28
49
|
if (!cachedTemplateUuid) {
|
|
29
50
|
const template = await client.workflows.findEvaluationTemplate();
|
|
@@ -34,9 +55,9 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
34
55
|
cachedTemplateUuid = template.uuid;
|
|
35
56
|
logger.info(`Using workflow template: ${template.name} (${template.uuid})`);
|
|
36
57
|
}
|
|
37
|
-
// --- Build context data ---
|
|
58
|
+
// --- Build context data (targetUrl is the tunnel URL for localhost, original URL otherwise) ---
|
|
38
59
|
const contextData = {
|
|
39
|
-
targetUrl: originalUrl,
|
|
60
|
+
targetUrl: ctx.targetUrl ?? originalUrl,
|
|
40
61
|
goal: input.description,
|
|
41
62
|
};
|
|
42
63
|
// --- Build env (credentials/environment) ---
|
|
@@ -53,23 +74,11 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
53
74
|
env.password = input.password;
|
|
54
75
|
// --- Execute ---
|
|
55
76
|
if (progressCallback) {
|
|
56
|
-
await progressCallback({ progress:
|
|
77
|
+
await progressCallback({ progress: 3, total: 10, message: 'Queuing workflow execution...' });
|
|
57
78
|
}
|
|
58
79
|
const executeResponse = await client.workflows.executeWorkflow(cachedTemplateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
|
|
59
80
|
const executionUuid = executeResponse.executionUuid;
|
|
60
|
-
ngrokKeyId = executeResponse.ngrokKeyId ?? undefined;
|
|
61
81
|
logger.info(`Execution queued: ${executionUuid}`);
|
|
62
|
-
// --- Tunnel (after execute — backend returns tunnelKey, executionUuid is the subdomain) ---
|
|
63
|
-
if (ctx.isLocalhost) {
|
|
64
|
-
if (progressCallback) {
|
|
65
|
-
await progressCallback({ progress: 3, total: 10, message: 'Creating secure tunnel for localhost...' });
|
|
66
|
-
}
|
|
67
|
-
if (!executeResponse.tunnelKey) {
|
|
68
|
-
throw new Error('Backend did not return a tunnel key for localhost execution');
|
|
69
|
-
}
|
|
70
|
-
ctx = await ensureTunnel(ctx, executeResponse.tunnelKey, executionUuid);
|
|
71
|
-
logger.info(`Tunnel ready for ${originalUrl} (id: ${executionUuid})`);
|
|
72
|
-
}
|
|
73
82
|
// --- Poll ---
|
|
74
83
|
// nodeExecutions grows as each node completes: trigger → browser.setup → surfer.execute_task → browser.teardown
|
|
75
84
|
const NODE_PHASE_LABELS = {
|
|
@@ -93,11 +102,22 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
93
102
|
: exec.status;
|
|
94
103
|
await progressCallback({ progress, total: 10, message });
|
|
95
104
|
}
|
|
96
|
-
});
|
|
105
|
+
}, abortController.signal);
|
|
97
106
|
const duration = Date.now() - startTime;
|
|
98
107
|
// --- Format result ---
|
|
99
108
|
const outcome = finalExecution.state?.outcome ?? finalExecution.status;
|
|
100
109
|
const surferNode = finalExecution.nodeExecutions?.find(n => n.nodeType === 'surfer.execute_task');
|
|
110
|
+
// Log all node executions to diagnose what the backend returns
|
|
111
|
+
logger.info('Node executions raw data', {
|
|
112
|
+
nodeCount: finalExecution.nodeExecutions?.length ?? 0,
|
|
113
|
+
nodes: finalExecution.nodeExecutions?.map(n => ({
|
|
114
|
+
nodeId: n.nodeId,
|
|
115
|
+
nodeType: n.nodeType,
|
|
116
|
+
status: n.status,
|
|
117
|
+
outputKeys: n.outputData ? Object.keys(n.outputData) : [],
|
|
118
|
+
outputData: n.outputData,
|
|
119
|
+
})),
|
|
120
|
+
});
|
|
101
121
|
const responsePayload = {
|
|
102
122
|
outcome,
|
|
103
123
|
success: finalExecution.state?.success ?? false,
|
|
@@ -127,16 +147,40 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
127
147
|
const content = [
|
|
128
148
|
{ type: 'text', text: JSON.stringify(responsePayload, null, 2) },
|
|
129
149
|
];
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
150
|
+
// Search all node outputs for screenshot/gif URLs — not just the surfer node
|
|
151
|
+
const SCREENSHOT_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
|
|
152
|
+
const GIF_KEYS = ['runGif', 'gifUrl', 'gif', 'videoUrl', 'recordingUrl'];
|
|
153
|
+
let screenshotUrl = null;
|
|
154
|
+
let gifUrl = null;
|
|
155
|
+
for (const node of finalExecution.nodeExecutions ?? []) {
|
|
156
|
+
const data = node.outputData ?? {};
|
|
157
|
+
if (!screenshotUrl) {
|
|
158
|
+
for (const key of SCREENSHOT_KEYS) {
|
|
159
|
+
if (typeof data[key] === 'string' && data[key]) {
|
|
160
|
+
screenshotUrl = data[key];
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!gifUrl) {
|
|
166
|
+
for (const key of GIF_KEYS) {
|
|
167
|
+
if (typeof data[key] === 'string' && data[key]) {
|
|
168
|
+
gifUrl = data[key];
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (screenshotUrl && gifUrl)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
134
176
|
if (screenshotUrl) {
|
|
177
|
+
logger.info(`Embedding screenshot: ${screenshotUrl}`);
|
|
135
178
|
const img = await fetchImageAsBase64(screenshotUrl).catch(() => null);
|
|
136
179
|
if (img)
|
|
137
180
|
content.push(imageContentBlock(img.data, img.mimeType));
|
|
138
181
|
}
|
|
139
182
|
if (gifUrl) {
|
|
183
|
+
logger.info(`Embedding GIF/video: ${gifUrl}`);
|
|
140
184
|
const gif = await fetchImageAsBase64(gifUrl).catch(() => null);
|
|
141
185
|
if (gif)
|
|
142
186
|
content.push(imageContentBlock(gif.data, 'image/gif'));
|
|
@@ -152,9 +196,14 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
152
196
|
throw handleExternalServiceError(error, 'DebuggAI', 'test execution');
|
|
153
197
|
}
|
|
154
198
|
finally {
|
|
155
|
-
|
|
156
|
-
|
|
199
|
+
process.stdin.removeListener('close', onStdinClose);
|
|
200
|
+
// Tunnels stay alive for reuse — the 55-min auto-shutoff on TunnelManager
|
|
201
|
+
// fires revokeKey when the tunnel actually stops.
|
|
202
|
+
//
|
|
203
|
+
// Only revoke explicitly when we provisioned a key but tunnel creation failed
|
|
204
|
+
// (keyId set, ctx.tunnelId not set → key was never attached to a tunnel).
|
|
205
|
+
if (keyId && !ctx.tunnelId) {
|
|
206
|
+
client.revokeNgrokKey(keyId).catch(err => logger.warn(`Failed to revoke unused ngrok key ${keyId}: ${err}`));
|
|
157
207
|
}
|
|
158
|
-
releaseTunnel(ctx).catch(err => logger.warn(`Failed to stop tunnel: ${err}`));
|
|
159
208
|
}
|
|
160
209
|
}
|
package/dist/services/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createWorkflowsService } from "./workflows.js";
|
|
2
|
+
import { createTunnelsService } from "./tunnels.js";
|
|
2
3
|
import { AxiosTransport } from "../utils/axiosTransport.js";
|
|
3
4
|
import { config } from "../config/index.js";
|
|
4
5
|
/**
|
|
@@ -33,6 +34,7 @@ export class DebuggAIServerClient {
|
|
|
33
34
|
tx;
|
|
34
35
|
url;
|
|
35
36
|
workflows;
|
|
37
|
+
tunnels;
|
|
36
38
|
constructor(userApiKey) {
|
|
37
39
|
this.userApiKey = userApiKey;
|
|
38
40
|
// Note: init() is async and should be called separately
|
|
@@ -42,6 +44,7 @@ export class DebuggAIServerClient {
|
|
|
42
44
|
this.url = new URL(serverUrl);
|
|
43
45
|
this.tx = new DebuggTransport({ baseUrl: serverUrl, apiKey: this.userApiKey, tokenType: config.api.tokenType });
|
|
44
46
|
this.workflows = createWorkflowsService(this.tx);
|
|
47
|
+
this.tunnels = createTunnelsService(this.tx);
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Revoke an ngrok API key by its key ID.
|