@debugg-ai/debugg-ai-mcp 5.0.0 → 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 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
- ## [Unreleased] — BREAKING
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.imported === true` on successful ingestion. The `browserSession` block (HAR + console-log URLs, same shape as above) is also present on completed crawls.
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 `isUsable` and a `usableCount` |
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 an `authorizedCredentialHostsWarning` saying so instead of a silent success.
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 filter-mode response is paginated. Response shape:
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. Pass projectUuid (get it from search_projects) or invoke from a directory with a git origin.',
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}". Pass projectUuid explicitly.`,
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, 'create');
84
+ const hostsWarning = checkAuthorizedCredentialHostsEcho(input.authorizedCredentialHosts, env.authorizedCredentialHosts);
85
85
  if (hostsWarning) {
86
86
  payload.authorizedCredentialHostsWarning = hostsWarning;
87
- logger.warn(`create_environment: ${hostsWarning.message}`);
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}". Pass ${kind.toLowerCase()}Uuid explicitly.`,
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
- const usableCount = sessions.filter(s => s.isUsable).length;
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}. Start your dev server on that port before running probe_page. Probe result: ${probe.code} (${probe.detail ?? 'no detail'}).`,
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}. (Detail: ${msg})`);
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 Workflow Template not found (slug "${templateSlug}"). ` +
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 but traffic isn't reaching the dev server. ${health.detail ?? ''}`,
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
- : target.url,
362
- statusCode: typeof data.statusCode === 'number' ? data.statusCode : 0,
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 : 0,
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}. Start your dev server before running the suite. (${probe.code}: ${probe.detail ?? 'no detail'})`, { port, probeCode: probe.code, elapsedMs: probe.elapsedMs });
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}. (Detail: ${msg})${diag}`);
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}. (Detail: ${msg})`);
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 but traffic isn't reaching the dev server. ${health.detail ?? ''}`, { code: health.code, tunnelErrorCode: health.tunnelErrorCode, elapsedMs: health.elapsedMs });
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. Pass projectUuid (get via search_projects) or invoke from a directory with a git origin.');
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}". Pass projectUuid explicitly.`);
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
- filter: { uuid: input.uuid },
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,