@debugg-ai/debugg-ai-mcp 1.0.64 → 1.0.66

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/CHANGELOG.md ADDED
@@ -0,0 +1,184 @@
1
+ # Changelog
2
+
3
+ All notable changes to the DebuggAI MCP project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Fixed — tunnel provisioning flakiness surfaces as user-facing errors
11
+
12
+ - `check_app_in_browser` / `trigger_crawl` now automatically retry transient tunnel-provision failures (5xx, 408, 429, network errors like ECONNRESET) with exponential backoff (500ms → 1500ms → 3000ms, 3 attempts). Previously a single ngrok/backend blip forced the caller to manually retry the tool call. Bead `7nx`.
13
+ - Tunnel-provision error messages now carry structured diagnostic context — HTTP status, ngrok error code, backend `x-request-id`, retryable flag — so users have something actionable to file bug reports against instead of opaque "Tunnel setup failed". Bead `5wz`.
14
+ - 4xx auth/quota errors (401/403/404) fail fast without retry to avoid loops against a bad API key.
15
+ - New posthog telemetry event `tunnel.provision_retry` fires per retry attempt with outcome, status, and diagnostic fields so flaky provision rates become measurable.
16
+
17
+ ## [1.0.64] - 2026-04-23
18
+
19
+ > **⚠️ Semver violation — this is functionally a major release shipped as a patch.**
20
+ > The surface collapse below removes 14 tools. Callers pinned to `^1.0.63` will silently
21
+ > receive a breaking API on their next install. A republish as `2.0.0` (or a `2.0.0`
22
+ > bump with `1.0.64` deprecated) is recommended to restore semver discipline.
23
+
24
+ This is a **breaking release**. The MCP surface collapsed from 22 tools to 11 through a uniform `search_*` pattern plus credential-management consolidation into the environment tools. The full old→new mapping is below.
25
+
26
+ ### ⚠️ BREAKING CHANGES — 14 tools removed, replaced by 11-tool surface
27
+
28
+ | Removed tool | Replacement |
29
+ |---|---|
30
+ | `list_projects` | `search_projects({q?, page?, pageSize?})` (filter mode) |
31
+ | `get_project` | `search_projects({uuid})` (uuid mode — returns the curated detail shape) |
32
+ | `list_environments` | `search_environments({projectUuid?, q?, page?, pageSize?})` — credentials inlined per env |
33
+ | `get_environment` | `search_environments({uuid, projectUuid})` |
34
+ | `list_credentials` | `search_environments(...)` — credentials are inlined on each returned env (never include password) |
35
+ | `get_credential` | `search_environments({uuid, projectUuid})` — pull from the env's `credentials[]` |
36
+ | `create_credential` | `create_environment({name, url, credentials: [...]})` (seed on env create), or `update_environment({uuid, addCredentials: [...]})` |
37
+ | `update_credential` | `update_environment({uuid, updateCredentials: [{uuid, ...patch}]})` |
38
+ | `delete_credential` | `update_environment({uuid, removeCredentialIds: [uuid]})` |
39
+ | `list_teams` | `create_project({teamName, ...})` — backend name-resolved with exact-match + ambiguity handling |
40
+ | `list_repos` | `create_project({repoName, ...})` — same pattern |
41
+ | `list_executions` | `search_executions({status?, projectUuid?, page?, pageSize?})` |
42
+ | `get_execution` | `search_executions({uuid})` — full detail with `nodeExecutions` + state |
43
+ | `cancel_execution` | Dropped — backend spin-down is now automatic; no client action needed |
44
+
45
+ All `search_*` tools use a dual-mode signature: pass `{uuid}` for a single-record detail response, or pass filter params for a paginated summary list. 404 from the backend surfaces as `isError: true` with `{error: 'NotFound', message, uuid}`.
46
+
47
+ Credential mutations on `update_environment` execute as `remove → update → add` in a single call, so a freed label can be re-bound in one request. Per-cred failures surface in `credentialWarnings[]` without blocking the env update.
48
+
49
+ ### Added
50
+
51
+ - **`trigger_crawl` tool**: server-side browser-agent crawl to populate the project's knowledge graph. Returns `{executionId, status, targetUrl, durationMs, outcome?, crawlSummary?, knowledgeGraph?}` with `knowledgeGraph.imported` = true on successful KG ingestion. Supports localhost via automatic ngrok tunneling with per-process reuse.
52
+ - **`create_project` name-based resolution**: pass `teamName` instead of `teamUuid`, or `repoName` instead of `repoUuid`. Backend-side search with case-insensitive exact match. Returns `AmbiguousMatch` with candidates if multiple hits, `NotFound` if none.
53
+ - **`create_environment` credential seeding**: pass `credentials: [{label, username, password, role?}]` to create creds atomically with the env.
54
+ - **`update_environment` credential sub-actions**: `addCredentials[]`, `updateCredentials[]`, `removeCredentialIds[]` in one call.
55
+ - **`engines.node: ">=20.20.0"`** in `package.json`. Driven by `posthog-node@^5.26.0` requiring Node 20.20+.
56
+ - **Boot-smoke CI** (`.github/workflows/boot-smoke.yml`): matrix `{ubuntu, macos} × {Node 20, 22}` verifies the MCP server boots + completes `tools/list` with published-style spawn.
57
+ - **Eval runner tag filtering**: `--tag=<name>`, `--skip-tag=<name>`, `--flow=<csv>`; `--list` prints flows + tags. `--tag=fast` runs 12 non-browser flows in ~40s; `--tag=browser` runs heavy flows.
58
+ - **27 eval flows total** (up from 16 in prior unreleased work). New flows since the last published version: response-structure (20), tunnel reuse (21), long-running check (22), crawl triggers public + localhost + with-project (23/24/26), published-boot-smoke (25), localhost deep-path (27).
59
+ - **Response sanitization**: `check_app_in_browser` strips ngrok tunnel URLs from the full response including agent-authored `actionTrace[*].intent`.
60
+
61
+ ### Changed
62
+
63
+ - **Deferred API-key validation**: missing `DEBUGGAI_API_KEY` no longer crashes the subprocess at boot (the bug that surfaced in Claude Code as "Failed to reconnect to debugg-ai"). The server starts, `tools/list` succeeds, and the error surfaces only when a tool is actually invoked — as a structured `isError: true` response pointing the caller at the missing env var.
64
+ - **Boot-time behavior**: `index.ts` no longer calls `resolveProjectContext()` at startup. Project context resolves lazily on first tool call that needs it.
65
+ - **`services/projectContext.ts`**: promise-dedup pattern replaces the failure-caching singleton. Concurrent callers share one in-flight promise; results cached on success only, so transient network errors don't permanently disable context resolution.
66
+ - **Pagination mandatory on every list response**: `search_projects` / `search_environments` / `search_executions` accept optional `page` (1-indexed) and `pageSize` (default 20, max 200, oversized clamped). Response shape: `{filter, pageInfo: {page, pageSize, totalCount, totalPages, hasMore}, <items>}`.
67
+ - **Axios error handling**: handlers map `err.statusCode` (surfaced by the transport's response interceptor) to tool-level `NotFound` errors instead of checking `err.response?.status` which the interceptor strips.
68
+
69
+ ### Fixed
70
+
71
+ - **Progress-notification race** (bead `0bq`) in both `testPageChangesHandler` and `triggerCrawlHandler`: a progress callback firing after the handler resolved could tear down the stdio transport. Circuit breaker suppresses subsequent callbacks after the first throw; terminal-status detection emits the final `progress === total` notification inside `onUpdate` before the poll loop exits.
72
+ - **"Failed to reconnect to debugg-ai" UX** (bead `cma`): missing API key now surfaces as a per-tool-call error instead of a silent subprocess exit at boot. MCP clients see the server register normally and get a readable error only when a tool is actually invoked.
73
+ - **Credential role filter** (bead `hpo`): backend `?role=` filter on credentials list was returning all creds regardless. MCP now applies client-side role filtering as defense-in-depth.
74
+
75
+ ### Security invariants
76
+
77
+ - Passwords are write-only. No response body from any tool contains a password (verified by unit tests + eval flows 06/10/12/15).
78
+ - Tunnel URLs (`*.ngrok.debugg.ai`) are stripped from all `check_app_in_browser` responses including agent-authored text (verified by flow 05).
79
+ - 404s from the backend surface as `isError: true` with structured `{error: 'NotFound', ...}`, never as thrown exceptions.
80
+
81
+ ### Tool count
82
+
83
+ The server registers **11** tools (was 22 pre-collapse, 18 in the previous unreleased snapshot). Verified by eval flow `01-protocol.mjs` which locks the roster.
84
+
85
+ ## [1.0.15] - 2025-08-18
86
+
87
+ ### Added
88
+ - **Live Session Monitoring Tools**: Added 5 new MCP tools for real-time browser session monitoring
89
+ - `debugg_ai_start_live_session`: Launch live remote browser sessions with real-time monitoring
90
+ - `debugg_ai_stop_live_session`: Stop active live sessions
91
+ - `debugg_ai_get_live_session_status`: Monitor session status and health
92
+ - `debugg_ai_get_live_session_logs`: Retrieve console logs and network requests from live sessions
93
+ - `debugg_ai_get_live_session_screenshot`: Capture screenshots from active sessions
94
+ - **Enhanced Tunnel Management**: Complete rewrite of tunnel infrastructure with improved ngrok integration
95
+ - New `TunnelManager` service for high-level tunnel abstraction
96
+ - Automatic localhost URL detection and tunnel creation
97
+ - Better error handling and connection stability
98
+ - Integrated tunnel support in live session handlers
99
+ - **Browser Sessions Service**: New dedicated service for managing browser automation sessions
100
+ - **Comprehensive Test Infrastructure**: Added extensive test suite covering unit, integration, and end-to-end scenarios
101
+ - Handler tests for E2E suites and live sessions
102
+ - Backend services integration tests
103
+ - Network and MCP tools validation tests
104
+ - Mock infrastructure for reliable testing
105
+ - **Enhanced Project Analysis**: New utilities for analyzing codebases and extracting context
106
+ - **Improved Error Handling**: Centralized error management with structured error types
107
+ - **URL Parser Utilities**: Robust URL parsing and localhost detection capabilities
108
+ - **Configuration Management**: Centralized configuration system with environment-based settings
109
+ - **API Specification**: Complete OpenAPI specification for backend integration
110
+ - **GitHub Actions Workflows**: Automated publishing, version bumping, and validation workflows
111
+
112
+ ### Changed
113
+ - **Major Architecture Refactoring**: Reorganized services, handlers, and utilities into cleaner modular structure
114
+ - **Moved Tunnel Services**: Relocated tunnel management from `tunnels/` to `services/ngrok/` for better organization
115
+ - **Enhanced E2E Runner**: Improved test execution with better progress tracking and error handling
116
+ - **Updated Package Dependencies**: Upgraded to latest versions of core dependencies including MCP SDK
117
+ - **Improved Documentation**: Updated README with comprehensive setup and usage instructions
118
+ - **Enhanced Type Definitions**: Expanded type system with better validation schemas
119
+
120
+ ### Fixed
121
+ - **API Endpoint Updates**: Resolved compatibility issues with backend API changes
122
+ - **Image Support Improvements**: Enhanced handling of screenshots and visual test artifacts
123
+ - **Tunnel Connection Stability**: Fixed issues with ngrok tunnel reliability and reconnection
124
+ - **ES Module Compatibility**: Resolved module resolution issues for better Node.js compatibility
125
+
126
+ ### Security
127
+ - **License Addition**: Added Apache 2.0 license for proper open source compliance
128
+ - **Environment Variable Validation**: Enhanced validation of sensitive configuration data
129
+
130
+ ## [1.0.14] - 2025-06-09
131
+
132
+ ### Added
133
+ - Final screen shot included.
134
+
135
+ ## [1.0.12] - 2025-06-02
136
+
137
+ ### Added
138
+ - Readme docs issue
139
+
140
+ ## [1.0.11] - 2025-06-02
141
+
142
+ ### Added
143
+ - New readme with instructions on install, usage, etc.
144
+
145
+ ## [1.0.10] - 2025-05-29
146
+
147
+ ### Fixed
148
+ - Most MCP clients still don't support images. removed that as a response.
149
+
150
+
151
+ ## [1.0.7] - 2025-05-29
152
+
153
+ ### Fixed
154
+ - Fixed tunneling issues
155
+ - Remove notifications when a token is not provided in the original request
156
+
157
+ ## [1.0.2] - 2025-05-28
158
+
159
+ ### Fixed
160
+ - Fixed ES module path resolution issues
161
+ - Added proper shebang line to executable files
162
+ - Ensured executable permissions are set during build
163
+
164
+ ### Added
165
+ - Docker container support
166
+ - Improved error handling for E2E test runs
167
+
168
+ ## [1.0.1] - 2025-05-28
169
+
170
+ ### Fixed
171
+ - Fixed TypeScript configuration to target ES2022
172
+ - Resolved dependency issues with Zod library
173
+
174
+ ### Added
175
+ - Initial implementation of E2E test runner
176
+ - Integration with DebuggAI server client
177
+
178
+ ## [1.0.0] - 2025-05-28
179
+
180
+ ### Added
181
+ - Initial release of DebuggAI MCP
182
+ - Support for running UI tests via MCP protocol
183
+ - Integration with ngrok for tunnel creation
184
+ - Basic test reporting functionality
package/README.md CHANGED
@@ -8,6 +8,8 @@ AI-powered browser testing via the [Model Context Protocol](https://modelcontext
8
8
 
9
9
  ## Setup
10
10
 
11
+ **Requires Node.js 20.20.0 or later** (transitive requirement from `posthog-node@^5.26.0`).
12
+
11
13
  Get an API key at [debugg.ai](https://debugg.ai), then add to your MCP client config:
12
14
 
13
15
  ```json
@@ -32,16 +34,18 @@ docker run -i --rm --init -e DEBUGGAI_API_KEY=your_api_key quinnosha/debugg-ai-m
32
34
 
33
35
  ## Tools
34
36
 
35
- The server exposes **21** tools. The headline one is `check_app_in_browser`; the rest manage projects, environments, credentials, workflow execution history, and the teams/repos needed to create new projects.
37
+ The server exposes **11** tools grouped into Browser (2), Search (3), Projects (3), and Environments (3). The headline tool is `check_app_in_browser`; the rest manage projects, environments + their credentials, and execution history through a uniform `search_*` + CRUD pattern.
38
+
39
+ ### Browser
36
40
 
37
- ### `check_app_in_browser`
41
+ #### `check_app_in_browser`
38
42
 
39
- Runs an AI browser agent against your app. The agent navigates, interacts, and reports back with screenshots.
43
+ Runs an AI browser agent against your app. The agent navigates, interacts, and reports back with screenshots. Localhost URLs are auto-tunneled via ngrok.
40
44
 
41
45
  | Parameter | Type | Description |
42
46
  |-----------|------|-------------|
43
47
  | `description` | string **required** | What to test (natural language) |
44
- | `url` | string **required** | Target URL — a localhost URL (`http://localhost:3000`) is auto-tunneled via ngrok |
48
+ | `url` | string **required** | Target URL — `http://localhost:3000` is auto-tunneled |
45
49
  | `environmentId` | string | UUID of a specific environment |
46
50
  | `credentialId` | string | UUID of a specific credential |
47
51
  | `credentialRole` | string | Pick a credential by role (e.g. `admin`, `guest`) |
@@ -49,54 +53,43 @@ Runs an AI browser agent against your app. The agent navigates, interacts, and r
49
53
  | `password` | string | Password for login (ephemeral — not persisted) |
50
54
  | `repoName` | string | Override auto-detected git repo name (e.g. `my-org/my-repo`) |
51
55
 
52
- ### Project management
56
+ One focused check per call. The agent has a ~25-step internal budget; split broader suites across multiple calls.
53
57
 
54
- | Tool | Purpose |
55
- |------|---------|
56
- | `list_projects` | List projects accessible to your API key. Optional `q` for name/repo search. |
57
- | `get_project` | Fetch a project by `uuid`. Simplified shape (no team/runner internals). |
58
- | `create_project` | Create a new project. Needs `name`, `platform` (e.g. `web`), `teamUuid` (from `list_teams`), and `repoUuid` (from `list_repos`). |
59
- | `update_project` | PATCH a project's `name` or `description`. |
60
- | `delete_project` | Destructive delete. Cascades envs, creds, and history. |
58
+ #### `trigger_crawl`
61
59
 
62
- ### Teams and repos (prerequisites for `create_project`)
60
+ Fires a server-side browser-agent crawl to populate the project's knowledge graph. Localhost URLs tunnel automatically. Returns `{executionId, status, targetUrl, durationMs, outcome?, crawlSummary?, knowledgeGraph?}` with `knowledgeGraph.imported === true` on successful ingestion.
63
61
 
64
- | Tool | Purpose |
65
- |------|---------|
66
- | `list_teams` | Paginated list of teams accessible to the API key; optional `q` for search. |
67
- | `list_repos` | Paginated list of GitHub-linked repos; optional `q` for search. Use repos with `isGithubAuthorized: true` when creating a project. |
62
+ ### Search (dual-mode: uuid detail OR filtered list)
68
63
 
69
- ### Environment management (scoped to a project)
64
+ Each `search_*` tool has two modes. Pass `{uuid}` for a single-record detail response. Pass filter params for a paginated summary list. 404 from the backend surfaces as `isError: true` with `{error: 'NotFound', message, uuid}`.
70
65
 
71
- | Tool | Purpose |
72
- |------|---------|
73
- | `list_environments` | List envs for a project. Optional `q`, `projectUuid`. |
74
- | `create_environment` | Create a new env. Requires `name` + `url`. |
75
- | `get_environment` | Fetch an env by `uuid`. |
76
- | `update_environment` | PATCH `name` / `url` / `description`. |
77
- | `delete_environment` | Destructive delete. |
66
+ | Tool | UUID mode | Filter mode |
67
+ |------|-----------|-------------|
68
+ | `search_projects` | `{uuid}` curated project detail | `{q?, page?, pageSize?}` → paginated summaries |
69
+ | `search_environments` | `{uuid, projectUuid}` env with credentials inlined | `{projectUuid?, q?, page?, pageSize?}` → paginated envs, each with credentials array |
70
+ | `search_executions` | `{uuid}` full detail with `nodeExecutions` + state | `{status?, projectUuid?, page?, pageSize?}` → paginated summaries |
71
+
72
+ `projectUuid` is optional on `search_environments` if omitted, it auto-resolves from the git repo. Credentials are **always** returned without passwords.
78
73
 
79
- ### Credential management (scoped to an environment)
74
+ ### Projects
80
75
 
81
76
  | Tool | Purpose |
82
77
  |------|---------|
83
- | `list_credentials` | List creds. Optional `environmentId`, `q`, `role` (server-side filter). **Never returns passwords.** |
84
- | `create_credential` | Create a cred. Requires `environmentId`, `label`, `username`, `password`; optional `role`. |
85
- | `get_credential` | Fetch by `uuid` + `environmentId`. |
86
- | `update_credential` | Partial PATCH. Pass `password` to rotate — it is never echoed back. |
87
- | `delete_credential` | Destructive delete. |
78
+ | `create_project` | Requires `name` + `platform`. Team and repo resolve by **either** uuid **or** name: pass `teamUuid` OR `teamName`, and `repoUuid` OR `repoName`. Name resolution is case-insensitive exact match; `NotFound` if none, `AmbiguousMatch` with candidates if multiple. |
79
+ | `update_project` | PATCH `name`, `description`. |
80
+ | `delete_project` | Destructive cascades environments, credentials, and execution history. |
88
81
 
89
- ### Workflow execution history
82
+ ### Environments (credential sub-actions folded in)
90
83
 
91
84
  | Tool | Purpose |
92
85
  |------|---------|
93
- | `list_executions` | Paginated history. Optional `status`, `limit`. |
94
- | `get_execution` | Full detail for a single execution including node-level state. |
95
- | `cancel_execution` | Cancel an in-flight execution. |
86
+ | `create_environment` | Requires `name` + `url`. Optional `credentials: [{label, username, password, role?}]` seeds credentials in the same call. Per-cred failures surface in `credentialWarnings[]` without blocking env creation. |
87
+ | `update_environment` | PATCH env fields (`name`, `url`, `description`) plus credential sub-actions in one call: `addCredentials[]`, `updateCredentials: [{uuid, ...patch}]`, `removeCredentialIds: [uuid]`. Execution order: **remove → update → add** (freed labels can be re-added in one request). |
88
+ | `delete_environment` | Destructive cascades credentials. |
96
89
 
97
90
  ### Pagination
98
91
 
99
- Every `list_*` tool is paginated by default. Response shape:
92
+ Every filter-mode response is paginated. Response shape:
100
93
 
101
94
  ```json
102
95
  {
@@ -106,13 +99,32 @@ Every `list_*` tool is paginated by default. Response shape:
106
99
  }
107
100
  ```
108
101
 
109
- Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped) to any list tool. No tool ever silently truncates results.
102
+ Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped). No response is ever silently truncated.
110
103
 
111
104
  ### Security invariants
112
105
 
113
106
  - Passwords are write-only. They never appear in any response body from any tool.
114
107
  - Tunnel URLs (`*.ngrok.debugg.ai`) are stripped from all browser-agent responses, including agent-authored text.
115
108
  - 404s from the backend surface as `isError: true` with `{error: 'NotFound', ...}`, never as thrown exceptions.
109
+ - Missing `DEBUGGAI_API_KEY` surfaces as a structured tool error on first invocation — the server still registers and lists tools normally.
110
+
111
+ ## Migration from v1.x (breaking change in v2.0.0)
112
+
113
+ v2 collapsed a 22-tool surface to 11. Old-tool → new-tool mapping:
114
+
115
+ | Removed | Replacement |
116
+ |---------|-------------|
117
+ | `list_projects`, `get_project` | `search_projects` (uuid mode vs filter mode) |
118
+ | `list_environments`, `get_environment` | `search_environments` |
119
+ | `list_credentials`, `get_credential` | `search_environments` — credentials inline on each env |
120
+ | `create_credential` | `create_environment({credentials: [...]})` seed, or `update_environment({addCredentials: [...]})` |
121
+ | `update_credential` | `update_environment({updateCredentials: [{uuid, ...patch}]})` |
122
+ | `delete_credential` | `update_environment({removeCredentialIds: [uuid]})` |
123
+ | `list_teams`, `list_repos` | `create_project({teamName, repoName})` — name resolution with ambiguity handling |
124
+ | `list_executions`, `get_execution` | `search_executions` |
125
+ | `cancel_execution` | **Dropped** — backend spin-down is automatic |
126
+
127
+ Response-shape changes: the bare `count` field on list responses is gone — use `pageInfo.totalCount`.
116
128
 
117
129
  ## Configuration
118
130
 
@@ -20,7 +20,7 @@ export async function createEnvironmentHandler(input, _context) {
20
20
  if (!repoName) {
21
21
  const payload = {
22
22
  error: 'NoProjectResolved',
23
- message: 'No git repo detected and no projectUuid provided. Pass projectUuid (get it from list_projects) or invoke from a directory with a git origin.',
23
+ message: 'No git repo detected and no projectUuid provided. Pass projectUuid (get it from search_projects) or invoke from a directory with a git origin.',
24
24
  };
25
25
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
26
26
  }
@@ -8,6 +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 { TunnelProvisionError } from '../services/tunnels.js';
11
12
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
12
13
  import { detectRepoName } from '../utils/gitContext.js';
13
14
  const logger = new Logger({ module: 'testPageChangesHandler' });
@@ -96,14 +97,15 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
96
97
  else {
97
98
  let tunnel;
98
99
  try {
99
- tunnel = await client.tunnels.provision();
100
+ tunnel = await client.tunnels.provisionWithRetry();
100
101
  }
101
102
  catch (provisionError) {
102
103
  const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
104
+ const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
103
105
  throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
104
106
  `The remote browser needs a secure tunnel to reach your local dev server. ` +
105
107
  `Make sure your dev server is running on the specified port and try again. ` +
106
- `(Detail: ${msg})`);
108
+ `(Detail: ${msg})${diag}`);
107
109
  }
108
110
  keyId = tunnel.keyId;
109
111
  try {
@@ -14,6 +14,7 @@ import { config } from '../config/index.js';
14
14
  import { Logger } from '../utils/logger.js';
15
15
  import { handleExternalServiceError } from '../utils/errors.js';
16
16
  import { DebuggAIServerClient } from '../services/index.js';
17
+ import { TunnelProvisionError } from '../services/tunnels.js';
17
18
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
18
19
  const logger = new Logger({ module: 'triggerCrawlHandler' });
19
20
  const TEMPLATE_KEYWORD = 'raw crawl';
@@ -61,13 +62,14 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
61
62
  else {
62
63
  let tunnel;
63
64
  try {
64
- tunnel = await client.tunnels.provision();
65
+ tunnel = await client.tunnels.provisionWithRetry();
65
66
  }
66
67
  catch (provisionError) {
67
68
  const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
69
+ const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
68
70
  throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}. ` +
69
71
  `The remote browser needs a secure tunnel to reach your local dev server. ` +
70
- `(Detail: ${msg})`);
72
+ `(Detail: ${msg})${diag}`);
71
73
  }
72
74
  keyId = tunnel.keyId;
73
75
  ctx = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
@@ -191,7 +191,7 @@ export class DebuggAIServerClient {
191
191
  * List environments for a project. Paginated.
192
192
  * Optional q filters by name via backend ?search=.
193
193
  * The bare-array variant (no pagination) is still used internally by
194
- * list_credentials when iterating across all envs.
194
+ * search_environments when iterating across all envs to inline credentials.
195
195
  */
196
196
  async listEnvironmentsForProject(projectUuid, q) {
197
197
  if (!this.tx)
@@ -298,8 +298,8 @@ export class DebuggAIServerClient {
298
298
  /**
299
299
  * List credentials for a specific environment. Unpaginated (fetches up to
300
300
  * backend max pageSize). q filters label/username server-side via ?search=;
301
- * role filters server-side. Used internally by list_credentials when
302
- * iterating across envs.
301
+ * role filters server-side. Used internally by search_environments when
302
+ * inlining credentials on each env in a page.
303
303
  */
304
304
  async listCredentialsForEnvironment(projectUuid, envUuid, q, role) {
305
305
  if (!this.tx)
@@ -3,11 +3,97 @@
3
3
  * Provisions short-lived ngrok keys for MCP-managed tunnel setup.
4
4
  * Called before executeWorkflow so the tunnel URL is known before execution starts.
5
5
  */
6
- export const createTunnelsService = (tx) => ({
7
- async provision(purpose = 'workflow') {
8
- const response = await tx.post('api/v1/tunnels/', { purpose });
6
+ import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
7
+ /**
8
+ * Typed error thrown by provision() when the backend/ngrok path fails.
9
+ * Carries diagnostic fields a retry wrapper (bead 7nx) can use to decide
10
+ * whether to retry, and that handler error messages can surface so users
11
+ * have something actionable to file bug reports against.
12
+ */
13
+ export class TunnelProvisionError extends Error {
14
+ status;
15
+ code;
16
+ requestId;
17
+ networkCode;
18
+ retryable;
19
+ constructor(opts) {
20
+ super(opts.message);
21
+ this.name = 'TunnelProvisionError';
22
+ this.status = opts.status;
23
+ this.code = opts.code;
24
+ this.requestId = opts.requestId;
25
+ this.networkCode = opts.networkCode;
26
+ this.retryable = opts.retryable;
27
+ }
28
+ /**
29
+ * Stable one-line suffix for user-facing error messages.
30
+ * Example: '(status: 503, request-id: abc123, retryable)' or '(network: ECONNRESET, retryable)'.
31
+ */
32
+ diagnosticSuffix() {
33
+ const parts = [];
34
+ if (this.status != null)
35
+ parts.push(`status: ${this.status}`);
36
+ if (this.code)
37
+ parts.push(`code: ${this.code}`);
38
+ if (this.requestId)
39
+ parts.push(`request-id: ${this.requestId}`);
40
+ if (this.networkCode)
41
+ parts.push(`network: ${this.networkCode}`);
42
+ parts.push(this.retryable ? 'retryable' : 'not-retryable');
43
+ return `(${parts.join(', ')})`;
44
+ }
45
+ }
46
+ /**
47
+ * Classify an axios-interceptor-rewritten error (or any thrown Error) into a
48
+ * TunnelProvisionError with retryable semantics. Called from provision().
49
+ *
50
+ * Retryable: 5xx, 408 (request timeout), 429 (rate limit), and any network
51
+ * error (no response received — ECONNRESET / ECONNREFUSED / timeout).
52
+ * Not retryable: 4xx other than 408/429 — those indicate auth/quota/input
53
+ * problems that won't self-heal on the same API key.
54
+ */
55
+ export function classifyProvisionError(err) {
56
+ const e = err;
57
+ const message = e?.message ? String(e.message) : 'Tunnel provisioning failed';
58
+ const status = typeof e?.statusCode === 'number' ? e.statusCode : undefined;
59
+ const data = e?.responseData;
60
+ const code = data && typeof data === 'object' && typeof data.code === 'string' ? data.code : undefined;
61
+ const headers = e?.responseHeaders;
62
+ const requestId = headers && typeof headers === 'object'
63
+ ? ((headers['x-request-id'] || headers['X-Request-Id']) ?? undefined)
64
+ : undefined;
65
+ const networkCode = typeof e?.networkCode === 'string' ? e.networkCode : undefined;
66
+ let retryable;
67
+ if (status == null) {
68
+ retryable = true;
69
+ }
70
+ else if (status >= 500) {
71
+ retryable = true;
72
+ }
73
+ else if (status === 408 || status === 429) {
74
+ retryable = true;
75
+ }
76
+ else {
77
+ retryable = false;
78
+ }
79
+ return new TunnelProvisionError({ message, status, code, requestId, networkCode, retryable });
80
+ }
81
+ const DEFAULT_BACKOFF_MS = [500, 1500, 3000];
82
+ const DEFAULT_MAX_ATTEMPTS = 3;
83
+ export const createTunnelsService = (tx) => {
84
+ async function provision(purpose = 'workflow') {
85
+ let response;
86
+ try {
87
+ response = await tx.post('api/v1/tunnels/', { purpose });
88
+ }
89
+ catch (err) {
90
+ throw classifyProvisionError(err);
91
+ }
9
92
  if (!response?.tunnelId || !response?.tunnelKey) {
10
- throw new Error('Tunnel provisioning failed: missing tunnelId or tunnelKey in response');
93
+ throw new TunnelProvisionError({
94
+ message: 'Tunnel provisioning returned a success response missing tunnelId or tunnelKey',
95
+ retryable: false,
96
+ });
11
97
  }
12
98
  return {
13
99
  tunnelId: response.tunnelId,
@@ -15,5 +101,48 @@ export const createTunnelsService = (tx) => ({
15
101
  keyId: response.keyId,
16
102
  expiresAt: response.expiresAt,
17
103
  };
18
- },
19
- });
104
+ }
105
+ async function provisionWithRetry(opts = {}) {
106
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
107
+ const backoff = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
108
+ const sleep = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
109
+ let lastErr;
110
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
111
+ try {
112
+ const result = await provision(opts.purpose);
113
+ if (attempt > 1) {
114
+ Telemetry.capture(TelemetryEvents.TUNNEL_PROVISION_RETRY, {
115
+ attempt,
116
+ outcome: 'success',
117
+ });
118
+ }
119
+ return result;
120
+ }
121
+ catch (err) {
122
+ const e = err instanceof TunnelProvisionError ? err : classifyProvisionError(err);
123
+ lastErr = e;
124
+ const isLastAttempt = attempt >= maxAttempts;
125
+ const willRetry = e.retryable && !isLastAttempt;
126
+ Telemetry.capture(TelemetryEvents.TUNNEL_PROVISION_RETRY, {
127
+ attempt,
128
+ outcome: willRetry ? 'will-retry' : 'giving-up',
129
+ status: e.status,
130
+ code: e.code,
131
+ requestId: e.requestId,
132
+ networkCode: e.networkCode,
133
+ retryable: e.retryable,
134
+ });
135
+ if (!willRetry)
136
+ throw e;
137
+ const waitMs = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 0;
138
+ await sleep(waitMs);
139
+ }
140
+ }
141
+ // Unreachable in practice — loop always returns or throws.
142
+ throw lastErr ?? new TunnelProvisionError({
143
+ message: 'provisionWithRetry exhausted attempts without a classified error',
144
+ retryable: false,
145
+ });
146
+ }
147
+ return { provision, provisionWithRetry };
148
+ };
@@ -1,6 +1,6 @@
1
1
  import { CreateProjectInputSchema } from '../types/index.js';
2
2
  import { createProjectHandler } from '../handlers/createProjectHandler.js';
3
- const DESCRIPTION = `Create a new DebuggAI project. Required: name, platform (e.g. "web"), teamUuid (from list_teams), repoUuid (from list_repos). Returns {created: true, project: {uuid, name, slug, platform, repoName, ...}}. The repo must be GitHub-linked to the account. Use list_teams + list_repos first to discover valid UUIDs.`;
3
+ const DESCRIPTION = `Create a new DebuggAI project. Required: name, platform (e.g. "web"), plus a team and a repo. Team and repo each accept EITHER a UUID or a name: pass teamUuid OR teamName, and repoUuid OR repoName. Name resolution does a backend search with case-insensitive exact match (returns AmbiguousMatch with candidates on multiple hits, NotFound on no hit). The repo must be GitHub-linked to the account. Returns {created: true, project: {uuid, name, slug, platform, repoName, ...}}.`;
4
4
  export function buildCreateProjectTool() {
5
5
  return {
6
6
  name: 'create_project',
@@ -11,10 +11,12 @@ export function buildCreateProjectTool() {
11
11
  properties: {
12
12
  name: { type: 'string', description: 'Project name. Required.', minLength: 1 },
13
13
  platform: { type: 'string', description: 'Platform (e.g. "web"). Required.', minLength: 1 },
14
- teamUuid: { type: 'string', description: 'Team UUID (from list_teams). Required.' },
15
- repoUuid: { type: 'string', description: 'GitHub repo UUID (from list_repos). Required repo must be GitHub-linked.' },
14
+ teamUuid: { type: 'string', description: 'Team UUID. Provide teamUuid OR teamName, not both.' },
15
+ teamName: { type: 'string', description: 'Team name (backend-resolved, case-insensitive exact match). Provide teamUuid OR teamName, not both.' },
16
+ repoUuid: { type: 'string', description: 'GitHub repo UUID. Provide repoUuid OR repoName, not both. Repo must be GitHub-linked.' },
17
+ repoName: { type: 'string', description: 'GitHub repo name, e.g. "org/repo" (backend-resolved, case-insensitive exact match). Provide repoUuid OR repoName, not both.' },
16
18
  },
17
- required: ['name', 'platform', 'teamUuid', 'repoUuid'],
19
+ required: ['name', 'platform'],
18
20
  additionalProperties: false,
19
21
  },
20
22
  };
@@ -35,9 +35,14 @@ export class AxiosTransport {
35
35
  const newErr = new Error(String(msg));
36
36
  newErr.statusCode = err.response?.status;
37
37
  newErr.responseData = data;
38
+ newErr.responseHeaders = err.response?.headers;
38
39
  return Promise.reject(newErr);
39
40
  }
40
- return Promise.reject(new Error(err.message || 'Unknown Axios error'));
41
+ // Network-class error (no response received) — preserve err.code (ECONNRESET, etc.)
42
+ const networkErr = new Error(err.message || 'Unknown Axios error');
43
+ if (err.code)
44
+ networkErr.networkCode = err.code;
45
+ return Promise.reject(networkErr);
41
46
  });
42
47
  // Request → snake_case
43
48
  this.axios.interceptors.request.use((cfg) => {
@@ -53,5 +53,6 @@ export const TelemetryEvents = {
53
53
  TOOL_FAILED: 'tool.failed',
54
54
  WORKFLOW_EXECUTED: 'workflow.executed',
55
55
  TUNNEL_PROVISIONED: 'tunnel.provisioned',
56
+ TUNNEL_PROVISION_RETRY: 'tunnel.provision_retry',
56
57
  TUNNEL_STOPPED: 'tunnel.stopped',
57
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,10 @@
10
10
  "node": ">=20.20.0"
11
11
  },
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "CHANGELOG.md",
15
+ "README.md",
16
+ "LICENSE"
14
17
  ],
15
18
  "scripts": {
16
19
  "lint": "eslint .",