@debugg-ai/debugg-ai-mcp 4.4.1 → 6.0.0
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 +61 -1
- package/README.md +32 -14
- package/dist/handlers/createEnvironmentHandler.js +4 -4
- package/dist/handlers/createProjectHandler.js +1 -1
- package/dist/handlers/environmentSessionsHandler.js +2 -18
- package/dist/handlers/probePageHandler.js +10 -13
- package/dist/handlers/runTestSuiteHandler.js +8 -9
- package/dist/handlers/searchEnvironmentsHandler.js +3 -6
- package/dist/handlers/searchExecutionsHandler.js +3 -11
- package/dist/handlers/searchProjectsHandler.js +1 -6
- package/dist/handlers/testPageChangesHandler.js +88 -183
- package/dist/handlers/triggerCrawlHandler.js +52 -44
- package/dist/handlers/updateEnvironmentHandler.js +2 -2
- package/dist/index.js +1 -1
- package/dist/services/caddy/caddyProxy.js +7 -6
- package/dist/services/index.js +18 -21
- package/dist/services/tunnel/debuggTransport.js +9 -9
- package/dist/services/tunnel/protocol/constants.js +3 -3
- package/dist/services/tunnel/transport.js +28 -9
- package/dist/services/{ngrok → tunnel}/tunnelFaultInjection.js +9 -15
- package/dist/services/{ngrok → tunnel}/tunnelManager.js +65 -131
- package/dist/services/{ngrok → tunnel}/tunnelRegistry.js +2 -2
- package/dist/services/tunnels.js +64 -46
- package/dist/services/verdictAdapter.js +167 -41
- package/dist/services/workflows.js +10 -1
- package/dist/tools/testPageChanges.js +1 -1
- package/dist/utils/authorizedCredentialHosts.js +8 -20
- package/dist/utils/confirmDestructive.js +1 -2
- package/dist/utils/errors.js +4 -5
- package/dist/utils/imageUtils.js +2 -2
- package/dist/utils/localReachability.js +62 -341
- package/dist/utils/resolveProject.js +1 -1
- package/dist/utils/telemetry.js +4 -4
- package/dist/utils/tunnelContext.js +11 -9
- package/dist/utils/tunnelDisposition.js +51 -69
- package/dist/utils/tunnelDomains.js +30 -21
- package/dist/utils/urlParser.js +38 -6
- package/package.json +2 -3
- package/dist/services/ngrok/error.js +0 -3
- package/dist/services/ngrok/ngrokAgentInspector.js +0 -210
- package/dist/services/ngrok/ngrokAgentSession.js +0 -49
- package/dist/services/ngrok/types.js +0 -1
- package/dist/services/tunnel/ngrokTransport.js +0 -165
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,67 @@ All notable changes to the DebuggAI MCP project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [6.0.0] — 2026-10-04 — BREAKING
|
|
9
|
+
|
|
10
|
+
### Changed — responses are the backend's data, not MCP-authored text
|
|
11
|
+
|
|
12
|
+
Every tool response now relays the backend's fields under the backend's names,
|
|
13
|
+
with its reasons verbatim. The MCP authors text only for what it observed itself
|
|
14
|
+
(its poll deadline, a probe/tunnel failure, input validation).
|
|
15
|
+
|
|
16
|
+
- `check_app_in_browser`: `outcome` is `pass | fail | error`, or `null` with
|
|
17
|
+
`skipped: true` and the backend's `skipReason` verbatim for a run that never
|
|
18
|
+
attempted a test (the raw run outcome is never read). Removed `success`,
|
|
19
|
+
`failureCategory`, `stepsTaken` / `stepsBudget` / `stepsRemaining` (use the
|
|
20
|
+
relayed `budget: {maxSteps, usedSteps}`), `failedNode` (use `errorInfo`),
|
|
21
|
+
`surferOutput`, and the node-rebuilt `actionTrace` / `evaluation` fallbacks.
|
|
22
|
+
`evaluation` now carries only keys the verdict lacks (it is omitted when it
|
|
23
|
+
would repeat `outcome` / `reason`). `credentialWarning` is `{requested, used}`
|
|
24
|
+
(no `message`). A tunnel fault keeps `error`, `backendVerdict`, `detail`; its
|
|
25
|
+
`message` and `failureCategory: 'infrastructure'` are gone and `reason` states
|
|
26
|
+
the recorded marker and the probe result.
|
|
27
|
+
- `executions`: `outcome`, `verdict.outcome` and `evaluation.outcome` go through
|
|
28
|
+
the same `pass | fail | error` allowlist (null stays null for running rows and
|
|
29
|
+
for never-tested runs; a never-tested list row carries `skipReason`).
|
|
30
|
+
- `trigger_crawl`: a never-tested run reads `outcome: null`, `skipped: true`,
|
|
31
|
+
`skipReason`.
|
|
32
|
+
- `get` actions return `{project}` / `{execution}` / `{project, environment}` —
|
|
33
|
+
no echoed `filter`, no single-row `pageInfo`. `list` actions no longer echo
|
|
34
|
+
`filter`.
|
|
35
|
+
- `environment` `sessions` → `{environmentUuid, sessions}`; `clearSessions` →
|
|
36
|
+
`{environmentUuid, invalidated}` (no `note`, `scope`, `usableCount`).
|
|
37
|
+
- `authorizedCredentialHostsWarning` is `{requested, returned}` (no `message`).
|
|
38
|
+
- `trigger_crawl`: `crawlSummary` / `knowledgeGraph` carry only reported keys
|
|
39
|
+
(no derived `imported`, no `0` / `''` defaults).
|
|
40
|
+
- `probe_page`: an unreported `statusCode` / `loadTimeMs` / `finalUrl` is `null`,
|
|
41
|
+
not `0` or the input URL.
|
|
42
|
+
- `test_suite`: an unreported `runStatus` / run counts are `null` (no invented
|
|
43
|
+
`NEVER_RUN` / `PENDING` / `0`); `results` per-test `lastRun.outcome` goes
|
|
44
|
+
through the `pass | fail | error` allowlist. `run`: no `note`. Error messages no longer append advice
|
|
45
|
+
("Start your dev server…", "Common causes…", "Ensure the template is seeded…",
|
|
46
|
+
"Pass … explicitly", "Link this repo…").
|
|
47
|
+
|
|
48
|
+
### Fixed — localhost URLs inside goal text now point at the tunnel
|
|
49
|
+
|
|
50
|
+
`check_app_in_browser` rewrites every `http(s)://localhost[:port]` /
|
|
51
|
+
`127.0.0.1` origin inside the goal text onto the run's tunnel origin, keeping path, query and
|
|
52
|
+
fragment. The remote browser used to dial its own loopback for those URLs and the run was
|
|
53
|
+
recorded as the app failing. With no tunnel the text is left unchanged.
|
|
54
|
+
|
|
55
|
+
### Migration from 5.x
|
|
56
|
+
|
|
57
|
+
| 5.x field | 6.0.0 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `success` | removed — read `outcome` (`pass` / `fail` / `error`, or `null` + `skipped` + `skipReason`) |
|
|
60
|
+
| `failureCategory` | removed — read `outcome` and the backend `reason` |
|
|
61
|
+
| `stepsTaken` / `stepsBudget` / `stepsRemaining` | removed — read `budget: {maxSteps, usedSteps}` |
|
|
62
|
+
| `failedNode` | removed — read `errorInfo` |
|
|
63
|
+
| `surferOutput`, rebuilt `actionTrace` / `evaluation` | removed |
|
|
64
|
+
| `project` / `executions` / `environment` `get` → `projects[0]` / `executions[0]` / `environments[0]` + `pageInfo` + `filter` | `{project}` / `{execution}` / `{project, environment}` |
|
|
65
|
+
| `list` → `filter` echo | removed |
|
|
66
|
+
| `*Warning.message`, `note`, error-message advice | removed — structured fields only |
|
|
67
|
+
|
|
68
|
+
## Earlier changes (recorded before versioned headings resumed)
|
|
9
69
|
|
|
10
70
|
### Changed — one ngrok tunnel per session instead of one per local port
|
|
11
71
|
|
package/README.md
CHANGED
|
@@ -13,9 +13,10 @@ AI-powered browser testing via the [Model Context Protocol](https://modelcontext
|
|
|
13
13
|
**Testing `http://localhost:...` URLs requires the `caddy` binary** — `check_app_in_browser`,
|
|
14
14
|
`probe_page`, and `trigger_crawl` tunnel localhost targets through a local Caddy reverse proxy.
|
|
15
15
|
This installs automatically: the `@radically-straightforward/caddy` npm dependency downloads a
|
|
16
|
-
pinned Caddy release for your platform during `npm install`/`npx
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
pinned Caddy release for your platform during `npm install`/`npx` — nothing to install yourself in
|
|
17
|
+
the normal case. It is now the **only** binary this package downloads; the tunnel client itself is
|
|
18
|
+
pure TypeScript — it replaced the `ngrok` package, which fetched the ngrok agent.
|
|
19
|
+
If that download never ran (`npm install --ignore-scripts`, an offline/air-gapped install), point `CADDY_BIN` at
|
|
19
20
|
your own install (`brew install caddy` / `apt install caddy` / see
|
|
20
21
|
[caddyserver.com/docs/install](https://caddyserver.com/docs/install)) — missing it surfaces as a
|
|
21
22
|
clear error on the first localhost-URL call, not a silent hang. Public-URL calls, every
|
|
@@ -63,7 +64,7 @@ The server exposes **8** tools: three **Browser** tools plus one **action-based*
|
|
|
63
64
|
|
|
64
65
|
#### `check_app_in_browser`
|
|
65
66
|
|
|
66
|
-
Runs an AI browser agent against your app. The agent navigates, interacts, and reports back with screenshots. Localhost URLs are auto-tunneled
|
|
67
|
+
Runs an AI browser agent against your app. The agent navigates, interacts, and reports back with screenshots. Localhost URLs are auto-tunneled through the debugg tunnel server.
|
|
67
68
|
|
|
68
69
|
| Parameter | Type | Description |
|
|
69
70
|
|-----------|------|-------------|
|
|
@@ -117,8 +118,7 @@ Results report the identity actually used, so a wrong one is visible rather than
|
|
|
117
118
|
],
|
|
118
119
|
"credentialWarning": {
|
|
119
120
|
"requested": "qa+invitefix@example.com",
|
|
120
|
-
"used": ["qatest123@example.com"]
|
|
121
|
-
"message": "This run signed in with an environment default credential even though '…' was specified. …"
|
|
121
|
+
"used": ["qatest123@example.com"]
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
@@ -142,7 +142,7 @@ URLs are short-lived presigned S3 — refetch the parent execution via `executio
|
|
|
142
142
|
|
|
143
143
|
#### `trigger_crawl`
|
|
144
144
|
|
|
145
|
-
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?, browserSession?}` with `knowledgeGraph.
|
|
145
|
+
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?, browserSession?}` with `knowledgeGraph.skipped === false` on successful ingestion. `crawlSummary` and `knowledgeGraph` carry only the keys the backend's crawl / import nodes reported — a missing count is absent, never a filled-in `0`. The `browserSession` block (HAR + console-log URLs, same shape as above) is also present on completed crawls.
|
|
146
146
|
|
|
147
147
|
#### `probe_page`
|
|
148
148
|
|
|
@@ -183,12 +183,12 @@ Team and repo resolve by **either** uuid **or** name (case-insensitive exact mat
|
|
|
183
183
|
| `create` | `{name, url, description?, projectUuid?, credentials?, authorizedCredentialHosts?}` | Created env (optionally seeds credentials) |
|
|
184
184
|
| `update` | `{uuid, name?, url?, description?, addCredentials?, updateCredentials?, removeCredentialIds?, authorizedCredentialHosts?}` | Patched env; credential ops run **remove → update → add** |
|
|
185
185
|
| `delete` | `{uuid, projectUuid?, confirm?}` | Deletes env (cascades credentials) — **requires confirmation** |
|
|
186
|
-
| `sessions` | `{uuid, username?, credentialId?}` | Captured login sessions the env holds, per account, with
|
|
186
|
+
| `sessions` | `{uuid, username?, credentialId?}` | Captured login sessions the env holds, per account, each with its own `isUsable` |
|
|
187
187
|
| `clearSessions` | `{uuid, username?, credentialId?, confirm?}` | Invalidates them so the next run logs in for real — **unscoped clears require confirmation** |
|
|
188
188
|
|
|
189
189
|
`projectUuid` auto-resolves from the git repo when omitted. Per-cred failures surface in `credentialWarnings[]` without blocking the env op.
|
|
190
190
|
|
|
191
|
-
`authorizedCredentialHosts` lists hosts where a run may enter this environment's credentials besides the app's own host — **for cross-domain SSO, add the IdP host here** (e.g. `["auth.example.com"]`). Bare hostnames only: no scheme, path, port or wildcard (subdomains of the app's host are already in scope). On `update` it replaces the list; `[]` clears it. `get`/`list` return it when the server supports it. The response echoes the saved list; if the server did not persist it (older servers ignore the field), the result carries
|
|
191
|
+
`authorizedCredentialHosts` lists hosts where a run may enter this environment's credentials besides the app's own host — **for cross-domain SSO, add the IdP host here** (e.g. `["auth.example.com"]`). Bare hostnames only: no scheme, path, port or wildcard (subdomains of the app's host are already in scope). On `update` it replaces the list; `[]` clears it. `get`/`list` return it when the server supports it. The response echoes the saved list; if the server did not persist it (older servers ignore the field), the result carries `authorizedCredentialHostsWarning: {requested, returned}` instead of a silent success.
|
|
192
192
|
|
|
193
193
|
`sessions` / `clearSessions` manage the warm authenticated sessions the backend reuses to skip login (see [Session reuse](#session-reuse-why-a-check-can-report-no-login-form)). Session contents are never returned — a session cookie is a bearer credential. `clearSessions` marks sessions invalid rather than deleting the rows, so reuse stops immediately while the capture history stays readable.
|
|
194
194
|
|
|
@@ -221,11 +221,10 @@ Team and repo resolve by **either** uuid **or** name (case-insensitive exact mat
|
|
|
221
221
|
|
|
222
222
|
### Pagination
|
|
223
223
|
|
|
224
|
-
Every
|
|
224
|
+
Every `list` response is paginated. Response shape:
|
|
225
225
|
|
|
226
226
|
```json
|
|
227
227
|
{
|
|
228
|
-
"filter": { "...echoed query params..." },
|
|
229
228
|
"pageInfo": { "page": 1, "pageSize": 20, "totalCount": 47, "totalPages": 3, "hasMore": true },
|
|
230
229
|
"<items>": [ ... ]
|
|
231
230
|
}
|
|
@@ -233,6 +232,8 @@ Every filter-mode response is paginated. Response shape:
|
|
|
233
232
|
|
|
234
233
|
Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped). No response is ever silently truncated.
|
|
235
234
|
|
|
235
|
+
A `get` returns the one object under its singular name — `{project}`, `{execution}`, `{project, environment}` — with no `pageInfo`.
|
|
236
|
+
|
|
236
237
|
## Resources
|
|
237
238
|
|
|
238
239
|
Alongside tools, the server exposes the read-only entities as MCP **resources**
|
|
@@ -254,10 +255,27 @@ clients without resource support keep using the tools.
|
|
|
254
255
|
### Security invariants
|
|
255
256
|
|
|
256
257
|
- Passwords are write-only. They never appear in any response body from any tool.
|
|
257
|
-
- Tunnel URLs (`*.
|
|
258
|
+
- Tunnel URLs (`*.tunnel.debugg.ai`, and the retired `*.ngrok.debugg.ai` that historical runs still
|
|
259
|
+
reference) are stripped from all browser-agent responses, including agent-authored text.
|
|
258
260
|
- 404s from the backend surface as `isError: true` with `{error: 'NotFound', ...}`, never as thrown exceptions.
|
|
259
261
|
- Missing `DEBUGGAI_API_KEY` surfaces as a structured tool error on first invocation — the server still registers and lists tools normally.
|
|
260
262
|
|
|
263
|
+
## Migration to v6.0.0 (backend verdicts, no MCP-authored text)
|
|
264
|
+
|
|
265
|
+
A run verdict is `pass | fail | error`, with the backend's `reason` relayed verbatim. A run that
|
|
266
|
+
never attempted a test reads `outcome: null`, `skipped: true`, `skipReason`.
|
|
267
|
+
|
|
268
|
+
| Removed | Replacement |
|
|
269
|
+
|---------|-------------|
|
|
270
|
+
| `success` | `outcome` |
|
|
271
|
+
| `failureCategory` | `outcome` + `reason` |
|
|
272
|
+
| `stepsTaken` / `stepsBudget` / `stepsRemaining` | `budget: {maxSteps, usedSteps}` |
|
|
273
|
+
| `failedNode` | `errorInfo` |
|
|
274
|
+
| `get` → `projects[0]` / `executions[0]` / `environments[0]` + `pageInfo` + `filter` | `{project}` / `{execution}` / `{project, environment}` |
|
|
275
|
+
| `message` / `note` on warnings and results | structured fields only |
|
|
276
|
+
|
|
277
|
+
Full list: [CHANGELOG.md](CHANGELOG.md) 6.0.0.
|
|
278
|
+
|
|
261
279
|
## Migration to v3.0.0 (action-based tools)
|
|
262
280
|
|
|
263
281
|
v3 consolidated the 20 per-verb tools into 8 action-based tools. Old tool → new `tool {action}`:
|
|
@@ -341,11 +359,11 @@ flow against the advertised authorization server. The bearer is request-scoped
|
|
|
341
359
|
|
|
342
360
|
stdio installs need none of these.
|
|
343
361
|
|
|
344
|
-
**Multi-replica deployments (go/no-go before rollout):** tunnel state (the
|
|
362
|
+
**Multi-replica deployments (go/no-go before rollout):** tunnel state (the session tunnel,
|
|
345
363
|
its Caddy instance, and its port-route lock) is in-process, keyed per caller by a hash of the
|
|
346
364
|
bearer token — there is no cross-process coordination. Running several replicas behind a plain
|
|
347
365
|
round-robin load balancer means one caller's calls can land on different replicas and mint one
|
|
348
|
-
tunnel **per replica they hit** instead of one for the whole session (
|
|
366
|
+
tunnel **per replica they hit** instead of one for the whole session (bounded by
|
|
349
367
|
replica count, self-healing via the existing 55-minute idle auto-shutoff — never a cross-session
|
|
350
368
|
correctness bug, since any single tool call stays on one replica for its whole duration). To get
|
|
351
369
|
the intended "one tunnel per session" behavior on a multi-replica HTTP deployment, configure
|
|
@@ -21,7 +21,7 @@ export async function createEnvironmentHandler(input, _context) {
|
|
|
21
21
|
if (!repoName) {
|
|
22
22
|
const payload = {
|
|
23
23
|
error: 'NoProjectResolved',
|
|
24
|
-
message: 'No git repo detected and no projectUuid provided.
|
|
24
|
+
message: 'No git repo detected and no projectUuid provided.',
|
|
25
25
|
};
|
|
26
26
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
27
27
|
}
|
|
@@ -29,7 +29,7 @@ export async function createEnvironmentHandler(input, _context) {
|
|
|
29
29
|
if (!project) {
|
|
30
30
|
const payload = {
|
|
31
31
|
error: 'NoProjectResolved',
|
|
32
|
-
message: `No DebuggAI project found for repo "${repoName}"
|
|
32
|
+
message: `No DebuggAI project found for repo "${repoName}".`,
|
|
33
33
|
};
|
|
34
34
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
35
35
|
}
|
|
@@ -81,10 +81,10 @@ export async function createEnvironmentHandler(input, _context) {
|
|
|
81
81
|
}
|
|
82
82
|
// Bead q4d4: only the echo proves the backend kept the hosts.
|
|
83
83
|
if (input.authorizedCredentialHosts !== undefined) {
|
|
84
|
-
const hostsWarning = checkAuthorizedCredentialHostsEcho(input.authorizedCredentialHosts, env.authorizedCredentialHosts
|
|
84
|
+
const hostsWarning = checkAuthorizedCredentialHostsEcho(input.authorizedCredentialHosts, env.authorizedCredentialHosts);
|
|
85
85
|
if (hostsWarning) {
|
|
86
86
|
payload.authorizedCredentialHostsWarning = hostsWarning;
|
|
87
|
-
logger.warn(`create_environment:
|
|
87
|
+
logger.warn(`create_environment: authorizedCredentialHosts not persisted`, hostsWarning);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
logger.toolComplete('create_environment', Date.now() - start);
|
|
@@ -32,7 +32,7 @@ function resolveName(name, candidates, kind) {
|
|
|
32
32
|
if (matches.length > 1) {
|
|
33
33
|
return {
|
|
34
34
|
error: 'AmbiguousMatch',
|
|
35
|
-
message: `Multiple ${kind.toLowerCase()}s match "${name}"
|
|
35
|
+
message: `Multiple ${kind.toLowerCase()}s match "${name}".`,
|
|
36
36
|
candidates: matches.map(m => ({ uuid: m.uuid, name: m.name })),
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -18,16 +18,7 @@ export async function listEnvironmentSessionsHandler(input, _context) {
|
|
|
18
18
|
// usableCount, not just the rows: "does this environment currently hold a
|
|
19
19
|
// session that will be restored?" is the question a caller is actually
|
|
20
20
|
// asking, and an expired row still reports status 'valid'.
|
|
21
|
-
|
|
22
|
-
return ok({
|
|
23
|
-
environmentUuid: input.uuid,
|
|
24
|
-
sessions,
|
|
25
|
-
pageInfo: { totalCount: sessions.length, usableCount },
|
|
26
|
-
note: sessions.length === 0
|
|
27
|
-
? 'No captured sessions — every run for this environment logs in for real.'
|
|
28
|
-
: `${usableCount} of ${sessions.length} session(s) would be restored instead of logging in. `
|
|
29
|
-
+ 'Use action "clearSessions" to force a real login, or pass freshSession:true on a single run.',
|
|
30
|
-
});
|
|
21
|
+
return ok({ environmentUuid: input.uuid, sessions });
|
|
31
22
|
}
|
|
32
23
|
catch (error) {
|
|
33
24
|
throw handleExternalServiceError(error, 'DebuggAI', 'environment.sessions');
|
|
@@ -45,14 +36,7 @@ export async function clearEnvironmentSessionsHandler(input, _context) {
|
|
|
45
36
|
const { invalidated } = await client.clearEnvironmentSessions(input.uuid, filters);
|
|
46
37
|
const scope = input.username ?? input.credentialId ?? 'all accounts';
|
|
47
38
|
logger.info(`environment.clearSessions: invalidated ${invalidated} session(s) for ${scope}`);
|
|
48
|
-
return ok({
|
|
49
|
-
environmentUuid: input.uuid,
|
|
50
|
-
invalidated,
|
|
51
|
-
scope,
|
|
52
|
-
note: invalidated === 0
|
|
53
|
-
? 'Nothing to clear — no usable captured session matched.'
|
|
54
|
-
: 'The next run for this identity will perform a real login and re-capture.',
|
|
55
|
-
});
|
|
39
|
+
return ok({ environmentUuid: input.uuid, invalidated });
|
|
56
40
|
}
|
|
57
41
|
catch (error) {
|
|
58
42
|
throw handleExternalServiceError(error, 'DebuggAI', 'environment.clearSessions');
|
|
@@ -106,7 +106,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
106
106
|
// Tunnel keys we provisioned this call (for cleanup if creation fails after key acquired).
|
|
107
107
|
// The provisions this call acquired, in target order. Provisions rather than
|
|
108
108
|
// bare key ids because revoking follows the transport the backend picked
|
|
109
|
-
// (
|
|
109
|
+
// (it routes to the tunnel's own revoke endpoint).
|
|
110
110
|
const acquiredProvisions = [];
|
|
111
111
|
// Progress budget: 1 pre-flight + 1 template + 1 execute + N per-target captures + 1 done
|
|
112
112
|
const TOTAL_STEPS = 3 + input.targets.length + 1;
|
|
@@ -126,7 +126,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
126
126
|
if (!probe.reachable) {
|
|
127
127
|
const payload = {
|
|
128
128
|
error: 'LocalServerUnreachable',
|
|
129
|
-
message: `No server listening on 127.0.0.1:${port}
|
|
129
|
+
message: `No server listening on 127.0.0.1:${port}.`,
|
|
130
130
|
detail: {
|
|
131
131
|
port,
|
|
132
132
|
probeCode: probe.code,
|
|
@@ -157,8 +157,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
157
157
|
catch (provisionError) {
|
|
158
158
|
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
159
159
|
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
160
|
-
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}
|
|
161
|
-
`(Detail: ${msg})${diag}`);
|
|
160
|
+
throw new Error(`Failed to provision tunnel for ${ctx.originalUrl}: ${msg}${diag}`);
|
|
162
161
|
}
|
|
163
162
|
acquiredProvisions.push(tunnel);
|
|
164
163
|
let tunneled;
|
|
@@ -167,7 +166,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
167
166
|
}
|
|
168
167
|
catch (tunnelError) {
|
|
169
168
|
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
170
|
-
throw new Error(`Tunnel creation failed for ${ctx.originalUrl}
|
|
169
|
+
throw new Error(`Tunnel creation failed for ${ctx.originalUrl}: ${msg}`);
|
|
171
170
|
}
|
|
172
171
|
// NOTE: the tunnel health probe used to run right here, immediately
|
|
173
172
|
// after ensureTunnel. Under the session-tunnel + Caddy model that is
|
|
@@ -198,9 +197,7 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
198
197
|
return client.workflows.findTemplateBySlug(templateSlug);
|
|
199
198
|
});
|
|
200
199
|
if (!templateUuid) {
|
|
201
|
-
throw new Error(`Page Probe
|
|
202
|
-
`Ensure the backend has that template seeded and accessible ` +
|
|
203
|
-
`(GET /api/v1/workflows/?slug=${templateSlug}).`);
|
|
200
|
+
throw new Error(`Page Probe workflow template not found (slug "${templateSlug}").`);
|
|
204
201
|
}
|
|
205
202
|
// ── Group targets by shared Caddy route (§4 multi-port batch decision) ──
|
|
206
203
|
// Single-group batches (the common case — one port, or all-public) behave
|
|
@@ -253,11 +250,11 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
253
250
|
if (!health.healthy) {
|
|
254
251
|
const payload = {
|
|
255
252
|
error: 'TunnelTrafficBlocked',
|
|
256
|
-
message: `Tunnel established
|
|
253
|
+
message: `Tunnel established; a request through it to 127.0.0.1:${extractLocalhostPort(groupCtx.originalUrl)} failed: ${health.detail ?? health.code}.`,
|
|
257
254
|
detail: {
|
|
258
255
|
code: health.code,
|
|
259
256
|
status: health.status,
|
|
260
|
-
|
|
257
|
+
tunnelErrorCode: health.tunnelErrorCode,
|
|
261
258
|
elapsedMs: health.elapsedMs,
|
|
262
259
|
},
|
|
263
260
|
};
|
|
@@ -358,10 +355,10 @@ export async function probePageHandler(input, context, rawProgressCallback) {
|
|
|
358
355
|
finalUrl: typeof data.capturedUrl === 'string' ? data.capturedUrl
|
|
359
356
|
: typeof data.finalUrl === 'string' ? data.finalUrl
|
|
360
357
|
: typeof data.url === 'string' ? data.url
|
|
361
|
-
:
|
|
362
|
-
statusCode: typeof data.statusCode === 'number' ? data.statusCode :
|
|
358
|
+
: null,
|
|
359
|
+
statusCode: typeof data.statusCode === 'number' ? data.statusCode : null,
|
|
363
360
|
title: typeof data.title === 'string' ? data.title : null,
|
|
364
|
-
loadTimeMs: typeof data.loadTimeMs === 'number' ? data.loadTimeMs :
|
|
361
|
+
loadTimeMs: typeof data.loadTimeMs === 'number' ? data.loadTimeMs : null,
|
|
365
362
|
consoleErrors: mapConsoleSlice(Array.isArray(data.consoleSlice) ? data.consoleSlice : []),
|
|
366
363
|
networkSummary: reaggregateByOriginPath(Array.isArray(data.networkSummary) ? data.networkSummary : []),
|
|
367
364
|
};
|
|
@@ -6,7 +6,7 @@ import { disposeUnhealthyTunnel } from '../utils/tunnelDisposition.js';
|
|
|
6
6
|
import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js';
|
|
7
7
|
import { extractLocalhostPort } from '../utils/urlParser.js';
|
|
8
8
|
import { buildContext, sanitizeResponseUrls } from '../utils/tunnelContext.js';
|
|
9
|
-
import { tunnelManager } from '../services/
|
|
9
|
+
import { tunnelManager } from '../services/tunnel/tunnelManager.js';
|
|
10
10
|
import { config } from '../config/index.js';
|
|
11
11
|
import { resolveProject, resolveTestSuite } from '../utils/resolveProject.js';
|
|
12
12
|
const logger = new Logger({ module: 'runTestSuiteHandler' });
|
|
@@ -51,7 +51,7 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
51
51
|
if (typeof port === 'number') {
|
|
52
52
|
const probe = await probeLocalPort(port);
|
|
53
53
|
if (!probe.reachable) {
|
|
54
|
-
return errorResp('LocalServerUnreachable', `No server listening on 127.0.0.1:${port}
|
|
54
|
+
return errorResp('LocalServerUnreachable', `No server listening on 127.0.0.1:${port}.`, { port, probeCode: probe.code, elapsedMs: probe.elapsedMs });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
if (config.devMode) {
|
|
@@ -68,7 +68,7 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
68
68
|
// and its PortLock would give this handler no real protection —
|
|
69
69
|
// the lock would release back to contention seconds after
|
|
70
70
|
// triggering a suite that goes on to use the port for much longer.
|
|
71
|
-
// So it gets its OWN tunnel, dialing
|
|
71
|
+
// So it gets its OWN tunnel, dialing the app directly, bypassing
|
|
72
72
|
// Caddy/PortLock entirely (never deduped/reused — always fresh).
|
|
73
73
|
let tunnel;
|
|
74
74
|
try {
|
|
@@ -77,7 +77,7 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
77
77
|
catch (provisionError) {
|
|
78
78
|
const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
|
|
79
79
|
const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
|
|
80
|
-
return errorResp('TunnelProvisionFailed', `Failed to provision tunnel for ${input.targetUrl}
|
|
80
|
+
return errorResp('TunnelProvisionFailed', `Failed to provision tunnel for ${input.targetUrl}: ${msg}${diag}`);
|
|
81
81
|
}
|
|
82
82
|
acquiredProvision = tunnel;
|
|
83
83
|
let dedicated;
|
|
@@ -86,9 +86,9 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
86
86
|
}
|
|
87
87
|
catch (tunnelError) {
|
|
88
88
|
const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
|
|
89
|
-
return errorResp('TunnelCreationFailed', `Tunnel creation failed for ${input.targetUrl}
|
|
89
|
+
return errorResp('TunnelCreationFailed', `Tunnel creation failed for ${input.targetUrl}: ${msg}`);
|
|
90
90
|
}
|
|
91
|
-
// Health probe — catches
|
|
91
|
+
// Health probe — catches an upstream-refused tunnel and bind mismatches before
|
|
92
92
|
// the remote agent wastes steps trying to reach the server.
|
|
93
93
|
if (dedicated.url) {
|
|
94
94
|
const health = await probeTunnelHealth(dedicated.url);
|
|
@@ -104,7 +104,7 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
104
104
|
// revoking a live tunnel's credential would kill what we preserved.
|
|
105
105
|
// (On the eviction branch markTunnelDead already revokes it.)
|
|
106
106
|
tunnelId = dedicated.tunnelId;
|
|
107
|
-
return errorResp('TunnelTrafficBlocked', `Tunnel established
|
|
107
|
+
return errorResp('TunnelTrafficBlocked', `Tunnel established; a request through it to 127.0.0.1:${port} failed: ${health.detail ?? health.code}.`, { code: health.code, tunnelErrorCode: health.tunnelErrorCode, elapsedMs: health.elapsedMs });
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
effectiveTargetUrl = dedicated.url;
|
|
@@ -119,13 +119,12 @@ export async function runTestSuiteHandler(input, _context) {
|
|
|
119
119
|
const responsePayload = {
|
|
120
120
|
...result,
|
|
121
121
|
...(tunnelId ? { tunnelActive: true, originalUrl: input.targetUrl } : {}),
|
|
122
|
-
note: 'Tests are running asynchronously. Use get_test_suite_results to check progress.',
|
|
123
122
|
};
|
|
124
123
|
// Bead debugg_ai_mcp-6cfv.7: this handler previously had NO sanitize call
|
|
125
124
|
// at all — safe only by accident of `result`'s narrow return type never
|
|
126
125
|
// having carried a tunnel hostname in practice. Add the same defensive
|
|
127
126
|
// pass every other handler already runs, so a future backend field that
|
|
128
|
-
// echoes back the (dedicated,
|
|
127
|
+
// echoes back the (dedicated, app-direct) tunnel URL can never leak it
|
|
129
128
|
// to a caller who only knows their own localhost address.
|
|
130
129
|
const sanitizedPayload = sanitizeCtx ? sanitizeResponseUrls(responsePayload, sanitizeCtx) : responsePayload;
|
|
131
130
|
return {
|
|
@@ -71,11 +71,11 @@ export async function searchEnvironmentsHandler(input, _context) {
|
|
|
71
71
|
if (!projectUuid) {
|
|
72
72
|
const repoName = detectRepoName();
|
|
73
73
|
if (!repoName) {
|
|
74
|
-
return noProjectResolved(pagination, 'No git repo detected and no projectUuid provided.
|
|
74
|
+
return noProjectResolved(pagination, 'No git repo detected and no projectUuid provided.');
|
|
75
75
|
}
|
|
76
76
|
const resolved = await client.findProjectByRepoName(repoName);
|
|
77
77
|
if (!resolved) {
|
|
78
|
-
return noProjectResolved(pagination, `No DebuggAI project found for repo "${repoName}"
|
|
78
|
+
return noProjectResolved(pagination, `No DebuggAI project found for repo "${repoName}".`);
|
|
79
79
|
}
|
|
80
80
|
projectUuid = resolved.uuid;
|
|
81
81
|
project = { uuid: resolved.uuid };
|
|
@@ -95,9 +95,7 @@ export async function searchEnvironmentsHandler(input, _context) {
|
|
|
95
95
|
const creds = await client.listCredentialsForEnvironment(projectUuid, input.uuid).catch(() => []);
|
|
96
96
|
const payload = {
|
|
97
97
|
project,
|
|
98
|
-
|
|
99
|
-
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
|
|
100
|
-
environments: [{ ...env, credentials: creds.map(stripPassword) }],
|
|
98
|
+
environment: { ...env, credentials: creds.map(stripPassword) },
|
|
101
99
|
};
|
|
102
100
|
logger.toolComplete('search_environments', Date.now() - start);
|
|
103
101
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
@@ -118,7 +116,6 @@ export async function searchEnvironmentsHandler(input, _context) {
|
|
|
118
116
|
}
|
|
119
117
|
const payload = {
|
|
120
118
|
project,
|
|
121
|
-
filter: { q: input.q ?? null },
|
|
122
119
|
pageInfo,
|
|
123
120
|
environments: withCreds,
|
|
124
121
|
};
|
|
@@ -13,6 +13,7 @@ import { handleExternalServiceError } from '../utils/errors.js';
|
|
|
13
13
|
import { DebuggAIServerClient } from '../services/index.js';
|
|
14
14
|
import { config } from '../config/index.js';
|
|
15
15
|
import { toPaginationParams } from '../utils/pagination.js';
|
|
16
|
+
import { withUserFacingOutcomes } from '../services/verdictAdapter.js';
|
|
16
17
|
import { fetchImageAsBase64, imageContentBlock, resourceLinkBlock, artifactResourceLinks } from '../utils/imageUtils.js';
|
|
17
18
|
const logger = new Logger({ module: 'searchExecutionsHandler' });
|
|
18
19
|
function notFound(uuid) {
|
|
@@ -33,11 +34,7 @@ export async function searchExecutionsHandler(input, _context) {
|
|
|
33
34
|
if (input.uuid) {
|
|
34
35
|
try {
|
|
35
36
|
const execution = await client.workflows.getExecution(input.uuid);
|
|
36
|
-
const payload = {
|
|
37
|
-
filter: { uuid: input.uuid },
|
|
38
|
-
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
|
|
39
|
-
executions: [execution],
|
|
40
|
-
};
|
|
37
|
+
const payload = { execution: withUserFacingOutcomes(execution) };
|
|
41
38
|
logger.toolComplete('search_executions', Date.now() - start);
|
|
42
39
|
const content = [
|
|
43
40
|
{ type: 'text', text: JSON.stringify(payload, null, 2) },
|
|
@@ -88,7 +85,6 @@ export async function searchExecutionsHandler(input, _context) {
|
|
|
88
85
|
? [resourceLinkBlock(gifUrl, `run-recording-${input.uuid}.gif`, {
|
|
89
86
|
mimeType: 'image/gif',
|
|
90
87
|
title: 'Run recording',
|
|
91
|
-
description: 'Animated recording of the execution (presigned URL — open or fetch on demand).',
|
|
92
88
|
})]
|
|
93
89
|
: []),
|
|
94
90
|
...artifactResourceLinks(execution.browserSession),
|
|
@@ -116,12 +112,8 @@ export async function searchExecutionsHandler(input, _context) {
|
|
|
116
112
|
pageSize: pagination.pageSize,
|
|
117
113
|
});
|
|
118
114
|
const payload = {
|
|
119
|
-
filter: {
|
|
120
|
-
status: input.status ?? null,
|
|
121
|
-
projectUuid: input.projectUuid ?? null,
|
|
122
|
-
},
|
|
123
115
|
pageInfo,
|
|
124
|
-
executions,
|
|
116
|
+
executions: executions.map(withUserFacingOutcomes),
|
|
125
117
|
};
|
|
126
118
|
logger.toolComplete('search_executions', Date.now() - start);
|
|
127
119
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
@@ -36,11 +36,7 @@ export async function searchProjectsHandler(input, _context) {
|
|
|
36
36
|
if (input.uuid) {
|
|
37
37
|
try {
|
|
38
38
|
const project = await client.getProject(input.uuid);
|
|
39
|
-
const payload = {
|
|
40
|
-
filter: { uuid: input.uuid },
|
|
41
|
-
pageInfo: { page: 1, pageSize: 1, totalCount: 1, totalPages: 1, hasMore: false },
|
|
42
|
-
projects: [project],
|
|
43
|
-
};
|
|
39
|
+
const payload = { project };
|
|
44
40
|
logger.toolComplete('search_projects', Date.now() - start);
|
|
45
41
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
46
42
|
}
|
|
@@ -53,7 +49,6 @@ export async function searchProjectsHandler(input, _context) {
|
|
|
53
49
|
const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
|
|
54
50
|
const { pageInfo, projects } = await client.listProjects(pagination, input.q);
|
|
55
51
|
const payload = {
|
|
56
|
-
filter: { q: input.q ?? null },
|
|
57
52
|
pageInfo,
|
|
58
53
|
projects: projects.map((p) => ({
|
|
59
54
|
uuid: p.uuid,
|