@debugg-ai/debugg-ai-mcp 5.0.0 → 6.0.1
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 +23 -7
- 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 +8 -11
- package/dist/handlers/runTestSuiteHandler.js +4 -5
- 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 +73 -164
- package/dist/handlers/triggerCrawlHandler.js +49 -41
- package/dist/handlers/updateEnvironmentHandler.js +2 -2
- package/dist/services/index.js +21 -14
- package/dist/services/verdictAdapter.js +167 -41
- package/dist/services/workflows.js +10 -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 +0 -1
- package/dist/utils/resolveProject.js +1 -1
- package/dist/utils/urlParser.js +33 -0
- package/package.json +1 -1
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
|
@@ -118,8 +118,7 @@ Results report the identity actually used, so a wrong one is visible rather than
|
|
|
118
118
|
],
|
|
119
119
|
"credentialWarning": {
|
|
120
120
|
"requested": "qa+invitefix@example.com",
|
|
121
|
-
"used": ["qatest123@example.com"]
|
|
122
|
-
"message": "This run signed in with an environment default credential even though '…' was specified. …"
|
|
121
|
+
"used": ["qatest123@example.com"]
|
|
123
122
|
}
|
|
124
123
|
```
|
|
125
124
|
|
|
@@ -143,7 +142,7 @@ URLs are short-lived presigned S3 — refetch the parent execution via `executio
|
|
|
143
142
|
|
|
144
143
|
#### `trigger_crawl`
|
|
145
144
|
|
|
146
|
-
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.
|
|
147
146
|
|
|
148
147
|
#### `probe_page`
|
|
149
148
|
|
|
@@ -184,12 +183,12 @@ Team and repo resolve by **either** uuid **or** name (case-insensitive exact mat
|
|
|
184
183
|
| `create` | `{name, url, description?, projectUuid?, credentials?, authorizedCredentialHosts?}` | Created env (optionally seeds credentials) |
|
|
185
184
|
| `update` | `{uuid, name?, url?, description?, addCredentials?, updateCredentials?, removeCredentialIds?, authorizedCredentialHosts?}` | Patched env; credential ops run **remove → update → add** |
|
|
186
185
|
| `delete` | `{uuid, projectUuid?, confirm?}` | Deletes env (cascades credentials) — **requires confirmation** |
|
|
187
|
-
| `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` |
|
|
188
187
|
| `clearSessions` | `{uuid, username?, credentialId?, confirm?}` | Invalidates them so the next run logs in for real — **unscoped clears require confirmation** |
|
|
189
188
|
|
|
190
189
|
`projectUuid` auto-resolves from the git repo when omitted. Per-cred failures surface in `credentialWarnings[]` without blocking the env op.
|
|
191
190
|
|
|
192
|
-
`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.
|
|
193
192
|
|
|
194
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.
|
|
195
194
|
|
|
@@ -222,11 +221,10 @@ Team and repo resolve by **either** uuid **or** name (case-insensitive exact mat
|
|
|
222
221
|
|
|
223
222
|
### Pagination
|
|
224
223
|
|
|
225
|
-
Every
|
|
224
|
+
Every `list` response is paginated. Response shape:
|
|
226
225
|
|
|
227
226
|
```json
|
|
228
227
|
{
|
|
229
|
-
"filter": { "...echoed query params..." },
|
|
230
228
|
"pageInfo": { "page": 1, "pageSize": 20, "totalCount": 47, "totalPages": 3, "hasMore": true },
|
|
231
229
|
"<items>": [ ... ]
|
|
232
230
|
}
|
|
@@ -234,6 +232,8 @@ Every filter-mode response is paginated. Response shape:
|
|
|
234
232
|
|
|
235
233
|
Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped). No response is ever silently truncated.
|
|
236
234
|
|
|
235
|
+
A `get` returns the one object under its singular name — `{project}`, `{execution}`, `{project, environment}` — with no `pageInfo`.
|
|
236
|
+
|
|
237
237
|
## Resources
|
|
238
238
|
|
|
239
239
|
Alongside tools, the server exposes the read-only entities as MCP **resources**
|
|
@@ -260,6 +260,22 @@ clients without resource support keep using the tools.
|
|
|
260
260
|
- 404s from the backend surface as `isError: true` with `{error: 'NotFound', ...}`, never as thrown exceptions.
|
|
261
261
|
- Missing `DEBUGGAI_API_KEY` surfaces as a structured tool error on first invocation — the server still registers and lists tools normally.
|
|
262
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
|
+
|
|
263
279
|
## Migration to v3.0.0 (action-based tools)
|
|
264
280
|
|
|
265
281
|
v3 consolidated the 20 per-verb tools into 8 action-based tools. Old tool → new `tool {action}`:
|
|
@@ -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');
|
|
@@ -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,7 +250,7 @@ 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,
|
|
@@ -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
|
};
|
|
@@ -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) {
|
|
@@ -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,7 +86,7 @@ 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
91
|
// Health probe — catches an upstream-refused tunnel and bind mismatches before
|
|
92
92
|
// the remote agent wastes steps trying to reach the server.
|
|
@@ -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,7 +119,6 @@ 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
|
|
@@ -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,
|