@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 CHANGED
@@ -1,42 +1,14 @@
1
- # Official MCP Server for Debugg AI
1
+ # Debugg AI — MCP Server
2
2
 
3
- **AI-powered browser testing and monitoring** via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). Gives AI agents the ability to run end-to-end browser tests, monitor live sessions, and validate UI changes against your running application.
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
- ![Test Create Account and Login](/assets/recordings/test-create-account-login.gif)
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
- > [Full Use Case Demo](https://debugg.ai/demo)
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
- **Or with Docker:**
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
- ## Tools
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
- - **Dashboard**: [app.debugg.ai](https://app.debugg.ai)
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, ensureTunnel, releaseTunnel, sanitizeResponseUrls, } from '../utils/tunnelContext.js';
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 ngrokKeyId;
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: 1, total: 10, message: 'Locating evaluation workflow template...' });
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: 2, total: 10, message: 'Queuing workflow execution...' });
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
- // Embed screenshot / GIF from the surfer node output when URLs are present
131
- const outputData = surferNode?.outputData ?? {};
132
- const screenshotUrl = outputData.finalScreenshot ?? outputData.screenshot ?? outputData.screenshotUrl ?? null;
133
- const gifUrl = outputData.runGif ?? outputData.gifUrl ?? null;
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
- if (ngrokKeyId) {
156
- client.revokeNgrokKey(ngrokKeyId).catch(err => logger.warn(`Failed to revoke ngrok key ${ngrokKeyId}: ${err}`));
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
  }
@@ -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.