@debugg-ai/debugg-ai-mcp 2.6.0 → 2.8.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
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added — E2E test suite management (8 new MCP tools)
11
+
12
+ Eight new tools for building and managing automated E2E test suites directly via MCP:
13
+
14
+ - `create_test_suite` — Create a named test suite for a project
15
+ - `search_test_suites` — List/search suites for a project with pagination and text filter
16
+ - `delete_test_suite` — Soft-delete (disable) a test suite
17
+ - `create_test_case` — Create a test case assigned to a suite (no auto-run)
18
+ - `update_test_case` — Update a test case's name, description, or agent task description
19
+ - `delete_test_case` — Soft-delete (disable) a test case
20
+ - `run_test_suite` — Trigger all test cases in a suite asynchronously
21
+ - `get_test_suite_results` — Fetch suite with per-test pass/fail outcomes and run history
22
+
23
+ All tools support name-based resolution (projectName, suiteName) with the same case-insensitive exact-match + ambiguity handling as existing tools. All backed by `/api/v1/test-suites/` and `/api/v1/e2e-tests/` endpoints on the DebuggAI backend. 80 new unit + integration tests added.
24
+
10
25
  ### Fixed — MCP now validates local reachability BEFORE hitting the backend (fixes 5-min false-pass regression)
11
26
 
12
27
  - `check_app_in_browser` and `trigger_crawl` now do a pre-flight TCP probe to `127.0.0.1:<port>` before provisioning a backend tunnel key. If the dev server isn't listening, we return a structured `LocalServerUnreachable` error in ~ms instead of letting the browser agent burn its 5-minute step budget on `ERR_NGROK_8012`. Bead `1om`.
@@ -0,0 +1,46 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { resolveProject, resolveTestSuite } from '../utils/resolveProject.js';
6
+ const logger = new Logger({ module: 'createTestCaseHandler' });
7
+ function errorResp(error, message, extra = {}) {
8
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
9
+ }
10
+ export async function createTestCaseHandler(input, _context) {
11
+ const start = Date.now();
12
+ logger.toolStart('create_test_case', input);
13
+ try {
14
+ const client = new DebuggAIServerClient(config.api.key);
15
+ await client.init();
16
+ let projectUuid = input.projectUuid;
17
+ if (!projectUuid) {
18
+ const resolved = await resolveProject(client, input.projectName);
19
+ if ('error' in resolved)
20
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
21
+ projectUuid = resolved.uuid;
22
+ }
23
+ let suiteUuid = input.suiteUuid;
24
+ if (!suiteUuid) {
25
+ const resolved = await resolveTestSuite(client, input.suiteName, projectUuid);
26
+ if ('error' in resolved)
27
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
28
+ suiteUuid = resolved.uuid;
29
+ }
30
+ const testCase = await client.createTestCase({
31
+ name: input.name,
32
+ description: input.description,
33
+ agentTaskDescription: input.agentTaskDescription,
34
+ suiteUuid,
35
+ projectUuid,
36
+ relativeUrl: input.relativeUrl,
37
+ maxSteps: input.maxSteps,
38
+ });
39
+ logger.toolComplete('create_test_case', Date.now() - start);
40
+ return { content: [{ type: 'text', text: JSON.stringify(testCase, null, 2) }] };
41
+ }
42
+ catch (error) {
43
+ logger.toolError('create_test_case', error, Date.now() - start);
44
+ throw handleExternalServiceError(error, 'DebuggAI', 'create_test_case');
45
+ }
46
+ }
@@ -0,0 +1,31 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { resolveProject } from '../utils/resolveProject.js';
6
+ const logger = new Logger({ module: 'createTestSuiteHandler' });
7
+ function errorResp(error, message, extra = {}) {
8
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
9
+ }
10
+ export async function createTestSuiteHandler(input, _context) {
11
+ const start = Date.now();
12
+ logger.toolStart('create_test_suite', input);
13
+ try {
14
+ const client = new DebuggAIServerClient(config.api.key);
15
+ await client.init();
16
+ let projectUuid = input.projectUuid;
17
+ if (!projectUuid) {
18
+ const resolved = await resolveProject(client, input.projectName);
19
+ if ('error' in resolved)
20
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
21
+ projectUuid = resolved.uuid;
22
+ }
23
+ const suite = await client.createTestSuite({ name: input.name, description: input.description, projectUuid });
24
+ logger.toolComplete('create_test_suite', Date.now() - start);
25
+ return { content: [{ type: 'text', text: JSON.stringify(suite, null, 2) }] };
26
+ }
27
+ catch (error) {
28
+ logger.toolError('create_test_suite', error, Date.now() - start);
29
+ throw handleExternalServiceError(error, 'DebuggAI', 'create_test_suite');
30
+ }
31
+ }
@@ -0,0 +1,20 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'deleteTestCaseHandler' });
6
+ export async function deleteTestCaseHandler(input, _context) {
7
+ const start = Date.now();
8
+ logger.toolStart('delete_test_case', input);
9
+ try {
10
+ const client = new DebuggAIServerClient(config.api.key);
11
+ await client.init();
12
+ await client.disableTestCase(input.testUuid);
13
+ logger.toolComplete('delete_test_case', Date.now() - start);
14
+ return { content: [{ type: 'text', text: JSON.stringify({ deleted: true, testUuid: input.testUuid }, null, 2) }] };
15
+ }
16
+ catch (error) {
17
+ logger.toolError('delete_test_case', error, Date.now() - start);
18
+ throw handleExternalServiceError(error, 'DebuggAI', 'delete_test_case');
19
+ }
20
+ }
@@ -0,0 +1,38 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { resolveProject, resolveTestSuite } from '../utils/resolveProject.js';
6
+ const logger = new Logger({ module: 'deleteTestSuiteHandler' });
7
+ function errorResp(error, message, extra = {}) {
8
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
9
+ }
10
+ export async function deleteTestSuiteHandler(input, _context) {
11
+ const start = Date.now();
12
+ logger.toolStart('delete_test_suite', input);
13
+ try {
14
+ const client = new DebuggAIServerClient(config.api.key);
15
+ await client.init();
16
+ let suiteUuid = input.suiteUuid;
17
+ if (!suiteUuid) {
18
+ let projectUuid = input.projectUuid;
19
+ if (!projectUuid) {
20
+ const resolved = await resolveProject(client, input.projectName);
21
+ if ('error' in resolved)
22
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
23
+ projectUuid = resolved.uuid;
24
+ }
25
+ const resolved = await resolveTestSuite(client, input.suiteName, projectUuid);
26
+ if ('error' in resolved)
27
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
28
+ suiteUuid = resolved.uuid;
29
+ }
30
+ await client.disableTestSuite(suiteUuid);
31
+ logger.toolComplete('delete_test_suite', Date.now() - start);
32
+ return { content: [{ type: 'text', text: JSON.stringify({ deleted: true, suiteUuid }, null, 2) }] };
33
+ }
34
+ catch (error) {
35
+ logger.toolError('delete_test_suite', error, Date.now() - start);
36
+ throw handleExternalServiceError(error, 'DebuggAI', 'delete_test_suite');
37
+ }
38
+ }
@@ -0,0 +1,38 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { resolveProject, resolveTestSuite } from '../utils/resolveProject.js';
6
+ const logger = new Logger({ module: 'getTestSuiteResultsHandler' });
7
+ function errorResp(error, message, extra = {}) {
8
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
9
+ }
10
+ export async function getTestSuiteResultsHandler(input, _context) {
11
+ const start = Date.now();
12
+ logger.toolStart('get_test_suite_results', input);
13
+ try {
14
+ const client = new DebuggAIServerClient(config.api.key);
15
+ await client.init();
16
+ let suiteUuid = input.suiteUuid;
17
+ if (!suiteUuid) {
18
+ let projectUuid = input.projectUuid;
19
+ if (!projectUuid) {
20
+ const resolved = await resolveProject(client, input.projectName);
21
+ if ('error' in resolved)
22
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
23
+ projectUuid = resolved.uuid;
24
+ }
25
+ const resolved = await resolveTestSuite(client, input.suiteName, projectUuid);
26
+ if ('error' in resolved)
27
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
28
+ suiteUuid = resolved.uuid;
29
+ }
30
+ const detail = await client.getTestSuiteDetail(suiteUuid);
31
+ logger.toolComplete('get_test_suite_results', Date.now() - start);
32
+ return { content: [{ type: 'text', text: JSON.stringify(detail, null, 2) }] };
33
+ }
34
+ catch (error) {
35
+ logger.toolError('get_test_suite_results', error, Date.now() - start);
36
+ throw handleExternalServiceError(error, 'DebuggAI', 'get_test_suite_results');
37
+ }
38
+ }
@@ -11,3 +11,11 @@ export * from './deleteEnvironmentHandler.js';
11
11
  export * from './updateProjectHandler.js';
12
12
  export * from './deleteProjectHandler.js';
13
13
  export * from './createProjectHandler.js';
14
+ export * from './createTestSuiteHandler.js';
15
+ export * from './searchTestSuitesHandler.js';
16
+ export * from './deleteTestSuiteHandler.js';
17
+ export * from './createTestCaseHandler.js';
18
+ export * from './updateTestCaseHandler.js';
19
+ export * from './deleteTestCaseHandler.js';
20
+ export * from './runTestSuiteHandler.js';
21
+ export * from './getTestSuiteResultsHandler.js';
@@ -24,6 +24,7 @@ import { extractLocalhostPort } from '../utils/urlParser.js';
24
24
  import { buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
25
25
  import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
26
26
  import { reaggregateByOriginPath, mapConsoleSlice } from '../utils/harSummarizer.js';
27
+ import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
27
28
  const logger = new Logger({ module: 'probePageHandler' });
28
29
  const TEMPLATE_KEYWORD = 'page probe';
29
30
  export async function probePageHandler(input, context, rawProgressCallback) {
@@ -256,12 +257,6 @@ export async function probePageHandler(input, context, rawProgressCallback) {
256
257
  result.surferPageUuid = data.surferPageUuid;
257
258
  }
258
259
  results.push(result);
259
- // Backend stores screenshots on the SurferPage row referenced by
260
- // surfer_page_uuid; the inline screenshotB64 is no longer in capture
261
- // output. v1: skip the per-result image content block; the screenshot
262
- // is reachable via search_executions detail's surfer_page_uuid → SurferPage.
263
- // (Future enhancement: fetch the SurferPage's presigned screenshot_url
264
- // when input.captureScreenshots is true.)
265
260
  }
266
261
  const responsePayload = {
267
262
  executionId: executionUuid,
@@ -284,11 +279,35 @@ export async function probePageHandler(input, context, rawProgressCallback) {
284
279
  }
285
280
  }
286
281
  logger.toolComplete('probe_page', duration);
287
- return {
288
- content: [
289
- { type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) },
290
- ],
291
- };
282
+ const responseContent = [
283
+ { type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) },
284
+ ];
285
+ // Embed screenshots when captureScreenshots is true. The backend may return
286
+ // screenshotB64 or a URL-keyed field on browser.capture outputData.
287
+ if (input.captureScreenshots) {
288
+ const SCREENSHOT_URL_KEYS = ['screenshotB64', 'screenshot', 'screenshotUrl', 'screenshotUri', 'finalScreenshot'];
289
+ for (const node of captureNodes) {
290
+ const data = node?.outputData ?? {};
291
+ if (typeof data.screenshotB64 === 'string' && data.screenshotB64) {
292
+ responseContent.push(imageContentBlock(data.screenshotB64, 'image/png'));
293
+ }
294
+ else {
295
+ let screenshotUrl = null;
296
+ for (const key of SCREENSHOT_URL_KEYS) {
297
+ if (key !== 'screenshotB64' && typeof data[key] === 'string' && data[key]) {
298
+ screenshotUrl = data[key];
299
+ break;
300
+ }
301
+ }
302
+ if (screenshotUrl) {
303
+ const img = await fetchImageAsBase64(screenshotUrl).catch(() => null);
304
+ if (img)
305
+ responseContent.push(imageContentBlock(img.data, img.mimeType));
306
+ }
307
+ }
308
+ }
309
+ }
310
+ return { content: responseContent };
292
311
  }
293
312
  catch (error) {
294
313
  const duration = Date.now() - startTime;
@@ -0,0 +1,116 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { TunnelProvisionError } from '../services/tunnels.js';
5
+ import { tunnelManager } from '../services/ngrok/tunnelManager.js';
6
+ import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js';
7
+ import { extractLocalhostPort } from '../utils/urlParser.js';
8
+ import { buildContext, findExistingTunnel, ensureTunnel } from '../utils/tunnelContext.js';
9
+ import { config } from '../config/index.js';
10
+ import { resolveProject, resolveTestSuite } from '../utils/resolveProject.js';
11
+ const logger = new Logger({ module: 'runTestSuiteHandler' });
12
+ function errorResp(error, message, extra = {}) {
13
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
14
+ }
15
+ export async function runTestSuiteHandler(input, _context) {
16
+ const start = Date.now();
17
+ logger.toolStart('run_test_suite', input);
18
+ const client = new DebuggAIServerClient(config.api.key);
19
+ await client.init();
20
+ let acquiredKeyId = null;
21
+ let tunnelId;
22
+ try {
23
+ let suiteUuid = input.suiteUuid;
24
+ if (!suiteUuid) {
25
+ let projectUuid = input.projectUuid;
26
+ if (!projectUuid) {
27
+ const resolved = await resolveProject(client, input.projectName);
28
+ if ('error' in resolved)
29
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
30
+ projectUuid = resolved.uuid;
31
+ }
32
+ const resolved = await resolveTestSuite(client, input.suiteName, projectUuid);
33
+ if ('error' in resolved)
34
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
35
+ suiteUuid = resolved.uuid;
36
+ }
37
+ // Resolve the effective target URL — tunnel if localhost, pass-through otherwise.
38
+ let effectiveTargetUrl = input.targetUrl;
39
+ if (input.targetUrl) {
40
+ const ctx = buildContext(input.targetUrl);
41
+ if (ctx.isLocalhost) {
42
+ const port = extractLocalhostPort(ctx.originalUrl);
43
+ if (typeof port === 'number') {
44
+ const probe = await probeLocalPort(port);
45
+ if (!probe.reachable) {
46
+ 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 });
47
+ }
48
+ }
49
+ // Reuse an existing tunnel for this port if one is already active.
50
+ const reused = findExistingTunnel(ctx);
51
+ if (reused) {
52
+ effectiveTargetUrl = reused.targetUrl ?? input.targetUrl;
53
+ tunnelId = reused.tunnelId;
54
+ }
55
+ else {
56
+ // Provision a new tunnel.
57
+ let tunnel;
58
+ try {
59
+ tunnel = await client.tunnels.provisionWithRetry();
60
+ }
61
+ catch (provisionError) {
62
+ const msg = provisionError instanceof Error ? provisionError.message : String(provisionError);
63
+ const diag = provisionError instanceof TunnelProvisionError ? ` ${provisionError.diagnosticSuffix()}` : '';
64
+ return errorResp('TunnelProvisionFailed', `Failed to provision tunnel for ${input.targetUrl}. (Detail: ${msg})${diag}`);
65
+ }
66
+ acquiredKeyId = tunnel.keyId;
67
+ let tunneled;
68
+ try {
69
+ tunneled = await ensureTunnel(ctx, tunnel.tunnelKey, tunnel.tunnelId, tunnel.keyId, () => client.revokeNgrokKey(tunnel.keyId));
70
+ }
71
+ catch (tunnelError) {
72
+ const msg = tunnelError instanceof Error ? tunnelError.message : String(tunnelError);
73
+ return errorResp('TunnelCreationFailed', `Tunnel creation failed for ${input.targetUrl}. (Detail: ${msg})`);
74
+ }
75
+ // Health probe — catches ERR_NGROK_8012 and bind mismatches before
76
+ // the remote agent wastes steps trying to reach the server.
77
+ if (tunneled.targetUrl) {
78
+ const health = await probeTunnelHealth(tunneled.targetUrl);
79
+ if (!health.healthy) {
80
+ if (tunneled.tunnelId) {
81
+ tunnelManager.stopTunnel(tunneled.tunnelId).catch((err) => logger.warn(`Failed to stop broken tunnel ${tunneled.tunnelId}: ${err}`));
82
+ }
83
+ return errorResp('TunnelTrafficBlocked', `Tunnel established but traffic isn't reaching the dev server. ${health.detail ?? ''}`, { code: health.code, ngrokErrorCode: health.ngrokErrorCode, elapsedMs: health.elapsedMs });
84
+ }
85
+ }
86
+ effectiveTargetUrl = tunneled.targetUrl ?? input.targetUrl;
87
+ tunnelId = tunneled.tunnelId;
88
+ }
89
+ logger.info(`run_test_suite: localhost detected, tunneled ${input.targetUrl} → ${effectiveTargetUrl}`);
90
+ }
91
+ }
92
+ const result = await client.runTestSuite(suiteUuid, { targetUrl: effectiveTargetUrl });
93
+ logger.toolComplete('run_test_suite', Date.now() - start);
94
+ return {
95
+ content: [{
96
+ type: 'text',
97
+ text: JSON.stringify({
98
+ ...result,
99
+ ...(tunnelId ? { tunnelActive: true, originalUrl: input.targetUrl } : {}),
100
+ note: 'Tests are running asynchronously. Use get_test_suite_results to check progress.',
101
+ }, null, 2),
102
+ }],
103
+ };
104
+ }
105
+ catch (error) {
106
+ logger.toolError('run_test_suite', error, Date.now() - start);
107
+ throw handleExternalServiceError(error, 'DebuggAI', 'run_test_suite');
108
+ }
109
+ finally {
110
+ // Tunnels are NOT torn down — reuse pattern + 55-min idle auto-shutoff.
111
+ // Only revoke an orphaned key (acquired but tunnel creation failed).
112
+ if (acquiredKeyId && !tunnelId) {
113
+ client.revokeNgrokKey(acquiredKeyId).catch((err) => logger.warn(`Failed to revoke unused ngrok key ${acquiredKeyId}: ${err}`));
114
+ }
115
+ }
116
+ }
@@ -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 { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
16
17
  const logger = new Logger({ module: 'searchExecutionsHandler' });
17
18
  function notFound(uuid) {
18
19
  return {
@@ -38,7 +39,53 @@ export async function searchExecutionsHandler(input, _context) {
38
39
  executions: [execution],
39
40
  };
40
41
  logger.toolComplete('search_executions', Date.now() - start);
41
- return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
42
+ const content = [
43
+ { type: 'text', text: JSON.stringify(payload, null, 2) },
44
+ ];
45
+ const SCREENSHOT_URL_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
46
+ const GIF_KEYS = ['runGif', 'gifUrl', 'gif', 'videoUrl', 'recordingUrl'];
47
+ const nodes = execution.nodeExecutions ?? [];
48
+ const subworkflowNode = nodes.find((n) => n.nodeType === 'subworkflow.run');
49
+ let screenshotEmbedded = false;
50
+ let screenshotUrl = null;
51
+ let gifUrl = null;
52
+ const screenshotB64 = subworkflowNode?.outputData?.screenshotB64;
53
+ if (typeof screenshotB64 === 'string' && screenshotB64) {
54
+ content.push(imageContentBlock(screenshotB64, 'image/png'));
55
+ screenshotEmbedded = true;
56
+ }
57
+ for (const node of nodes) {
58
+ const data = node.outputData ?? {};
59
+ if (!screenshotEmbedded && !screenshotUrl) {
60
+ for (const key of SCREENSHOT_URL_KEYS) {
61
+ if (typeof data[key] === 'string' && data[key]) {
62
+ screenshotUrl = data[key];
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ if (!gifUrl) {
68
+ for (const key of GIF_KEYS) {
69
+ if (typeof data[key] === 'string' && data[key]) {
70
+ gifUrl = data[key];
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ if ((screenshotEmbedded || screenshotUrl) && gifUrl)
76
+ break;
77
+ }
78
+ if (!screenshotEmbedded && screenshotUrl) {
79
+ const img = await fetchImageAsBase64(screenshotUrl).catch(() => null);
80
+ if (img)
81
+ content.push(imageContentBlock(img.data, img.mimeType));
82
+ }
83
+ if (gifUrl) {
84
+ const gif = await fetchImageAsBase64(gifUrl).catch(() => null);
85
+ if (gif)
86
+ content.push(imageContentBlock(gif.data, 'image/gif'));
87
+ }
88
+ return { content };
42
89
  }
43
90
  catch (err) {
44
91
  if (err?.statusCode === 404 || err?.response?.status === 404)
@@ -0,0 +1,36 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { resolveProject } from '../utils/resolveProject.js';
6
+ const logger = new Logger({ module: 'searchTestSuitesHandler' });
7
+ function errorResp(error, message, extra = {}) {
8
+ return { content: [{ type: 'text', text: JSON.stringify({ error, message, ...extra }, null, 2) }], isError: true };
9
+ }
10
+ export async function searchTestSuitesHandler(input, _context) {
11
+ const start = Date.now();
12
+ logger.toolStart('search_test_suites', input);
13
+ try {
14
+ const client = new DebuggAIServerClient(config.api.key);
15
+ await client.init();
16
+ let projectUuid = input.projectUuid;
17
+ if (!projectUuid) {
18
+ const resolved = await resolveProject(client, input.projectName);
19
+ if ('error' in resolved)
20
+ return errorResp(resolved.error, resolved.message, { candidates: resolved.candidates });
21
+ projectUuid = resolved.uuid;
22
+ }
23
+ const result = await client.listTestSuites({
24
+ projectUuid,
25
+ search: input.search,
26
+ page: input.page,
27
+ pageSize: input.pageSize,
28
+ });
29
+ logger.toolComplete('search_test_suites', Date.now() - start);
30
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
31
+ }
32
+ catch (error) {
33
+ logger.toolError('search_test_suites', error, Date.now() - start);
34
+ throw handleExternalServiceError(error, 'DebuggAI', 'search_test_suites');
35
+ }
36
+ }
@@ -0,0 +1,24 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'updateTestCaseHandler' });
6
+ export async function updateTestCaseHandler(input, _context) {
7
+ const start = Date.now();
8
+ logger.toolStart('update_test_case', input);
9
+ try {
10
+ const client = new DebuggAIServerClient(config.api.key);
11
+ await client.init();
12
+ const updated = await client.updateTestCase(input.testUuid, {
13
+ name: input.name,
14
+ description: input.description,
15
+ agentTaskDescription: input.agentTaskDescription,
16
+ });
17
+ logger.toolComplete('update_test_case', Date.now() - start);
18
+ return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
19
+ }
20
+ catch (error) {
21
+ logger.toolError('update_test_case', error, Date.now() - start);
22
+ throw handleExternalServiceError(error, 'DebuggAI', 'update_test_case');
23
+ }
24
+ }
@@ -430,6 +430,151 @@ export class DebuggAIServerClient {
430
430
  throw new Error('Client not initialized — call init() first');
431
431
  await this.tx.post('api/v1/ngrok/revoke/', { ngrokKeyId });
432
432
  }
433
+ // ── E2E Suite Management ──────────────────────────────────────────────────
434
+ async createTestSuite(input) {
435
+ if (!this.tx)
436
+ throw new Error('Client not initialized — call init() first');
437
+ const s = await this.tx.post('api/v1/test-suites/', {
438
+ name: input.name,
439
+ description: input.description,
440
+ project: input.projectUuid,
441
+ });
442
+ return this.mapTestSuite(s);
443
+ }
444
+ async listTestSuites(params) {
445
+ if (!this.tx)
446
+ throw new Error('Client not initialized — call init() first');
447
+ const { makePageInfo } = await import('../utils/pagination.js');
448
+ const page = params.page ?? 1;
449
+ const pageSize = params.pageSize ?? 20;
450
+ const query = { project: params.projectUuid, page, pageSize };
451
+ if (params.search)
452
+ query.search = params.search;
453
+ const response = await this.tx.get('api/v1/test-suites/', query);
454
+ return {
455
+ pageInfo: makePageInfo(page, pageSize, response?.count ?? 0, response?.next),
456
+ suites: (response?.results ?? []).map((s) => ({
457
+ uuid: s.uuid,
458
+ name: s.name,
459
+ description: s.description ?? null,
460
+ runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
461
+ testsCount: s.testsCount ?? s.tests_count ?? 0,
462
+ passRate: s.passRate ?? s.pass_rate ?? null,
463
+ lastRunAt: s.lastRunAt ?? s.last_run_at ?? null,
464
+ })),
465
+ };
466
+ }
467
+ async disableTestSuite(suiteUuid) {
468
+ if (!this.tx)
469
+ throw new Error('Client not initialized — call init() first');
470
+ await this.tx.post(`api/v1/test-suites/${suiteUuid}/disable/`, {});
471
+ return { uuid: suiteUuid, isDisabled: true };
472
+ }
473
+ async createTestCase(input) {
474
+ if (!this.tx)
475
+ throw new Error('Client not initialized — call init() first');
476
+ const body = {
477
+ name: input.name,
478
+ description: input.description,
479
+ agent_task_description: input.agentTaskDescription,
480
+ suite: input.suiteUuid,
481
+ project: input.projectUuid,
482
+ run: false,
483
+ };
484
+ if (input.relativeUrl)
485
+ body.relative_url = input.relativeUrl;
486
+ if (input.maxSteps)
487
+ body.max_steps = input.maxSteps;
488
+ const t = await this.tx.post('api/v1/e2e-tests/', body);
489
+ return {
490
+ uuid: t.uuid,
491
+ name: t.name,
492
+ description: t.description,
493
+ agentTaskDescription: t.agentTaskDescription ?? t.agent_task_description ?? '',
494
+ suite: t.suite ?? input.suiteUuid,
495
+ project: t.project ?? input.projectUuid,
496
+ runCount: t.runCount ?? t.run_count ?? 0,
497
+ };
498
+ }
499
+ async updateTestCase(testUuid, patch) {
500
+ if (!this.tx)
501
+ throw new Error('Client not initialized — call init() first');
502
+ const body = {};
503
+ if (patch.name !== undefined)
504
+ body.name = patch.name;
505
+ if (patch.description !== undefined)
506
+ body.description = patch.description;
507
+ if (patch.agentTaskDescription !== undefined)
508
+ body.agent_task_description = patch.agentTaskDescription;
509
+ const t = await this.tx.patch(`api/v1/e2e-tests/${testUuid}/`, body);
510
+ return {
511
+ uuid: t.uuid,
512
+ name: t.name,
513
+ description: t.description,
514
+ agentTaskDescription: t.agentTaskDescription ?? t.agent_task_description ?? '',
515
+ };
516
+ }
517
+ async disableTestCase(testUuid) {
518
+ if (!this.tx)
519
+ throw new Error('Client not initialized — call init() first');
520
+ await this.tx.post(`api/v1/e2e-tests/${testUuid}/disable/`, {});
521
+ return { uuid: testUuid, isDisabled: true };
522
+ }
523
+ async runTestSuite(suiteUuid, params) {
524
+ if (!this.tx)
525
+ throw new Error('Client not initialized — call init() first');
526
+ const body = {};
527
+ if (params.targetUrl)
528
+ body.target_url = params.targetUrl;
529
+ const s = await this.tx.post(`api/v1/test-suites/${suiteUuid}/run/`, body);
530
+ return {
531
+ suiteUuid,
532
+ runStatus: s?.runStatus ?? s?.run_status ?? 'PENDING',
533
+ testsTriggered: (s?.tests ?? []).length,
534
+ };
535
+ }
536
+ async getTestSuiteDetail(suiteUuid) {
537
+ if (!this.tx)
538
+ throw new Error('Client not initialized — call init() first');
539
+ const s = await this.tx.get(`api/v1/test-suites/${suiteUuid}/`);
540
+ const tests = s.tests ?? [];
541
+ return {
542
+ uuid: s.uuid,
543
+ name: s.name,
544
+ runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
545
+ testsCount: tests.length,
546
+ passRate: s.passRate ?? s.pass_rate ?? null,
547
+ lastRunAt: s.lastRunAt ?? s.last_run_at ?? null,
548
+ tests: tests.map((t) => {
549
+ // Backend returns cur_run (latest run) per test in the suite detail view
550
+ const lastRun = t.curRun ?? t.cur_run ?? t.lastRun ?? t.last_run ?? null;
551
+ return {
552
+ uuid: t.uuid,
553
+ name: t.name,
554
+ runCount: t.runCount ?? t.run_count ?? 0,
555
+ passedRunsCount: t.passedRunsCount ?? t.passed_runs_count ?? 0,
556
+ failedRunsCount: t.failedRunsCount ?? t.failed_runs_count ?? 0,
557
+ passRate: t.passRate ?? t.pass_rate ?? null,
558
+ lastRun: lastRun ? {
559
+ uuid: lastRun.uuid,
560
+ status: lastRun.status,
561
+ outcome: lastRun.outcome,
562
+ executionTime: lastRun.executionTime ?? lastRun.execution_time ?? null,
563
+ timestamp: lastRun.timestamp,
564
+ } : null,
565
+ };
566
+ }),
567
+ };
568
+ }
569
+ mapTestSuite(s) {
570
+ return {
571
+ uuid: s.uuid,
572
+ name: s.name,
573
+ description: s.description ?? null,
574
+ runStatus: s.runStatus ?? s.run_status ?? 'NEVER_RUN',
575
+ testsCount: s.testsCount ?? s.tests_count ?? 0,
576
+ };
577
+ }
433
578
  }
434
579
  /**
435
580
  * Create and initialize a service client
@@ -10,6 +10,7 @@ import { buildDeleteEnvironmentTool, buildValidatedDeleteEnvironmentTool } from
10
10
  import { buildUpdateProjectTool, buildValidatedUpdateProjectTool } from './updateProject.js';
11
11
  import { buildDeleteProjectTool, buildValidatedDeleteProjectTool } from './deleteProject.js';
12
12
  import { buildCreateProjectTool, buildValidatedCreateProjectTool } from './createProject.js';
13
+ import { buildCreateTestSuiteTool, buildValidatedCreateTestSuiteTool, buildSearchTestSuitesTool, buildValidatedSearchTestSuitesTool, buildDeleteTestSuiteTool, buildValidatedDeleteTestSuiteTool, buildCreateTestCaseTool, buildValidatedCreateTestCaseTool, buildUpdateTestCaseTool, buildValidatedUpdateTestCaseTool, buildDeleteTestCaseTool, buildValidatedDeleteTestCaseTool, buildRunTestSuiteTool, buildValidatedRunTestSuiteTool, buildGetTestSuiteResultsTool, buildValidatedGetTestSuiteResultsTool, } from './testSuiteTools.js';
13
14
  let _tools = null;
14
15
  let _validatedTools = null;
15
16
  const toolRegistry = new Map();
@@ -30,6 +31,14 @@ export function initTools(ctx) {
30
31
  buildDeleteProjectTool(),
31
32
  buildSearchExecutionsTool(),
32
33
  buildCreateProjectTool(),
34
+ buildCreateTestSuiteTool(),
35
+ buildSearchTestSuitesTool(),
36
+ buildDeleteTestSuiteTool(),
37
+ buildCreateTestCaseTool(),
38
+ buildUpdateTestCaseTool(),
39
+ buildDeleteTestCaseTool(),
40
+ buildRunTestSuiteTool(),
41
+ buildGetTestSuiteResultsTool(),
33
42
  ];
34
43
  const validated = [
35
44
  buildValidatedTestPageChangesTool(ctx),
@@ -44,6 +53,14 @@ export function initTools(ctx) {
44
53
  buildValidatedDeleteProjectTool(),
45
54
  buildValidatedSearchExecutionsTool(),
46
55
  buildValidatedCreateProjectTool(),
56
+ buildValidatedCreateTestSuiteTool(),
57
+ buildValidatedSearchTestSuitesTool(),
58
+ buildValidatedDeleteTestSuiteTool(),
59
+ buildValidatedCreateTestCaseTool(),
60
+ buildValidatedUpdateTestCaseTool(),
61
+ buildValidatedDeleteTestCaseTool(),
62
+ buildValidatedRunTestSuiteTool(),
63
+ buildValidatedGetTestSuiteResultsTool(),
47
64
  ];
48
65
  _tools = tools;
49
66
  _validatedTools = validated;
@@ -0,0 +1,183 @@
1
+ import { CreateTestSuiteInputSchema, SearchTestSuitesInputSchema, DeleteTestSuiteInputSchema, CreateTestCaseInputSchema, UpdateTestCaseInputSchema, DeleteTestCaseInputSchema, RunTestSuiteInputSchema, GetTestSuiteResultsInputSchema, } from '../types/index.js';
2
+ import { createTestSuiteHandler } from '../handlers/createTestSuiteHandler.js';
3
+ import { searchTestSuitesHandler } from '../handlers/searchTestSuitesHandler.js';
4
+ import { deleteTestSuiteHandler } from '../handlers/deleteTestSuiteHandler.js';
5
+ import { createTestCaseHandler } from '../handlers/createTestCaseHandler.js';
6
+ import { updateTestCaseHandler } from '../handlers/updateTestCaseHandler.js';
7
+ import { deleteTestCaseHandler } from '../handlers/deleteTestCaseHandler.js';
8
+ import { runTestSuiteHandler } from '../handlers/runTestSuiteHandler.js';
9
+ import { getTestSuiteResultsHandler } from '../handlers/getTestSuiteResultsHandler.js';
10
+ const PROJECT_PROPS = {
11
+ projectUuid: { type: 'string', description: 'Project UUID. Provide projectUuid OR projectName.' },
12
+ projectName: { type: 'string', description: 'Project name (case-insensitive exact match). Provide projectUuid OR projectName.' },
13
+ };
14
+ const SUITE_PROPS = {
15
+ suiteUuid: { type: 'string', description: 'Test suite UUID. Provide suiteUuid OR (suiteName + project identifier).' },
16
+ suiteName: { type: 'string', description: 'Test suite name (case-insensitive exact match). Requires projectUuid or projectName.' },
17
+ };
18
+ // ── create_test_suite ─────────────────────────────────────────────────────────
19
+ export function buildCreateTestSuiteTool() {
20
+ return {
21
+ name: 'create_test_suite',
22
+ title: 'Create Test Suite',
23
+ description: 'Create a named test suite for a project. A test suite is a collection of test cases that can be run together. Requires name, description, and a project identifier (projectUuid or projectName). Returns {uuid, name, description, runStatus, testsCount}.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ name: { type: 'string', description: 'Suite name. Required.', minLength: 1 },
28
+ description: { type: 'string', description: 'Suite description. Required.', minLength: 1 },
29
+ ...PROJECT_PROPS,
30
+ },
31
+ required: ['name', 'description'],
32
+ additionalProperties: false,
33
+ },
34
+ };
35
+ }
36
+ export function buildValidatedCreateTestSuiteTool() {
37
+ return { ...buildCreateTestSuiteTool(), inputSchema: CreateTestSuiteInputSchema, handler: createTestSuiteHandler };
38
+ }
39
+ // ── search_test_suites ────────────────────────────────────────────────────────
40
+ export function buildSearchTestSuitesTool() {
41
+ return {
42
+ name: 'search_test_suites',
43
+ title: 'Search Test Suites',
44
+ description: 'List and search test suites for a project. Returns paginated results with suite status, test counts, pass rates, and last run timestamps. Requires a project identifier (projectUuid or projectName). Optional: search text filter, page, pageSize (1-100, default 20).',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ ...PROJECT_PROPS,
49
+ search: { type: 'string', description: 'Optional text filter applied to suite name and description.' },
50
+ page: { type: 'number', description: 'Page number (default 1).', minimum: 1 },
51
+ pageSize: { type: 'number', description: 'Results per page (default 20, max 100).', minimum: 1, maximum: 100 },
52
+ },
53
+ additionalProperties: false,
54
+ },
55
+ };
56
+ }
57
+ export function buildValidatedSearchTestSuitesTool() {
58
+ return { ...buildSearchTestSuitesTool(), inputSchema: SearchTestSuitesInputSchema, handler: searchTestSuitesHandler };
59
+ }
60
+ // ── delete_test_suite ─────────────────────────────────────────────────────────
61
+ export function buildDeleteTestSuiteTool() {
62
+ return {
63
+ name: 'delete_test_suite',
64
+ title: 'Delete Test Suite',
65
+ description: 'Disable (soft-delete) a test suite. The suite and its tests are hidden from default list queries but not permanently removed. Accepts suiteUuid directly, or suiteName + project identifier for name-based lookup. Returns {deleted: true, suiteUuid}.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ ...SUITE_PROPS,
70
+ ...PROJECT_PROPS,
71
+ },
72
+ additionalProperties: false,
73
+ },
74
+ };
75
+ }
76
+ export function buildValidatedDeleteTestSuiteTool() {
77
+ return { ...buildDeleteTestSuiteTool(), inputSchema: DeleteTestSuiteInputSchema, handler: deleteTestSuiteHandler };
78
+ }
79
+ // ── create_test_case ──────────────────────────────────────────────────────────
80
+ export function buildCreateTestCaseTool() {
81
+ return {
82
+ name: 'create_test_case',
83
+ title: 'Create Test Case',
84
+ description: 'Create an individual test case and assign it to a test suite. The test is NOT automatically executed. Requires name, description, agentTaskDescription (the AI agent\'s goal), and suite + project identifiers. Optional: relativeUrl (must start with "/") and maxSteps (1-100). Returns {uuid, name, description, agentTaskDescription, suite, project, runCount}.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ name: { type: 'string', description: 'Test case name. Required.', minLength: 1 },
89
+ description: { type: 'string', description: 'Test case description. Required.', minLength: 1 },
90
+ agentTaskDescription: { type: 'string', description: 'Natural language description of what the AI agent should do and verify. Required.', minLength: 1 },
91
+ ...SUITE_PROPS,
92
+ ...PROJECT_PROPS,
93
+ relativeUrl: { type: 'string', description: 'Optional starting URL path relative to the app root, e.g. "/login". Must start with "/".' },
94
+ maxSteps: { type: 'number', description: 'Maximum agent steps (1-100).', minimum: 1, maximum: 100 },
95
+ },
96
+ required: ['name', 'description', 'agentTaskDescription'],
97
+ additionalProperties: false,
98
+ },
99
+ };
100
+ }
101
+ export function buildValidatedCreateTestCaseTool() {
102
+ return { ...buildCreateTestCaseTool(), inputSchema: CreateTestCaseInputSchema, handler: createTestCaseHandler };
103
+ }
104
+ // ── update_test_case ──────────────────────────────────────────────────────────
105
+ export function buildUpdateTestCaseTool() {
106
+ return {
107
+ name: 'update_test_case',
108
+ title: 'Update Test Case',
109
+ description: 'Update a test case\'s name, description, or agentTaskDescription. Requires testUuid. At least one of name, description, or agentTaskDescription must be provided. Returns the updated test case.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ testUuid: { type: 'string', description: 'UUID of the test case to update. Required.' },
114
+ name: { type: 'string', description: 'New name for the test case.', minLength: 1 },
115
+ description: { type: 'string', description: 'New description.', minLength: 1 },
116
+ agentTaskDescription: { type: 'string', description: 'New agent task description.', minLength: 1 },
117
+ },
118
+ required: ['testUuid'],
119
+ additionalProperties: false,
120
+ },
121
+ };
122
+ }
123
+ export function buildValidatedUpdateTestCaseTool() {
124
+ return { ...buildUpdateTestCaseTool(), inputSchema: UpdateTestCaseInputSchema, handler: updateTestCaseHandler };
125
+ }
126
+ // ── delete_test_case ──────────────────────────────────────────────────────────
127
+ export function buildDeleteTestCaseTool() {
128
+ return {
129
+ name: 'delete_test_case',
130
+ title: 'Delete Test Case',
131
+ description: 'Disable (soft-delete) a test case. The test is hidden from default list queries but not permanently removed. Requires testUuid. Returns {deleted: true, testUuid}.',
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties: {
135
+ testUuid: { type: 'string', description: 'UUID of the test case to delete. Required.' },
136
+ },
137
+ required: ['testUuid'],
138
+ additionalProperties: false,
139
+ },
140
+ };
141
+ }
142
+ export function buildValidatedDeleteTestCaseTool() {
143
+ return { ...buildDeleteTestCaseTool(), inputSchema: DeleteTestCaseInputSchema, handler: deleteTestCaseHandler };
144
+ }
145
+ // ── run_test_suite ────────────────────────────────────────────────────────────
146
+ export function buildRunTestSuiteTool() {
147
+ return {
148
+ name: 'run_test_suite',
149
+ title: 'Run Test Suite',
150
+ description: 'Trigger all test cases in a suite to run asynchronously. Accepts suiteUuid directly, or suiteName + project identifier. Optional: targetUrl to override the default test target. Returns {suiteUuid, runStatus, testsTriggered, note}. Use get_test_suite_results to poll for results.',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ ...SUITE_PROPS,
155
+ ...PROJECT_PROPS,
156
+ targetUrl: { type: 'string', description: 'Optional URL to run tests against (overrides default). Must be a full URL.' },
157
+ },
158
+ additionalProperties: false,
159
+ },
160
+ };
161
+ }
162
+ export function buildValidatedRunTestSuiteTool() {
163
+ return { ...buildRunTestSuiteTool(), inputSchema: RunTestSuiteInputSchema, handler: runTestSuiteHandler };
164
+ }
165
+ // ── get_test_suite_results ────────────────────────────────────────────────────
166
+ export function buildGetTestSuiteResultsTool() {
167
+ return {
168
+ name: 'get_test_suite_results',
169
+ title: 'Get Test Suite Results',
170
+ description: 'Fetch a test suite with full per-test results. Returns suite-level status (NEVER_RUN, PENDING, RUNNING, COMPLETED, ERROR), pass rate, last run timestamp, and per-test outcomes (PASS, FAIL, ERROR, TIMEOUT, etc.) with execution times. Accepts suiteUuid directly or suiteName + project identifier.',
171
+ inputSchema: {
172
+ type: 'object',
173
+ properties: {
174
+ ...SUITE_PROPS,
175
+ ...PROJECT_PROPS,
176
+ },
177
+ additionalProperties: false,
178
+ },
179
+ };
180
+ }
181
+ export function buildValidatedGetTestSuiteResultsTool() {
182
+ return { ...buildGetTestSuiteResultsTool(), inputSchema: GetTestSuiteResultsInputSchema, handler: getTestSuiteResultsHandler };
183
+ }
@@ -169,3 +169,54 @@ export const ProbePageInputSchema = z.object({
169
169
  captureScreenshots: z.boolean().default(true),
170
170
  repoName: z.string().optional(),
171
171
  }).strict();
172
+ // ── E2E Suite Management ──────────────────────────────────────────────────────
173
+ const projectIdentifier = {
174
+ projectUuid: z.string().uuid().optional(),
175
+ projectName: z.string().min(1).optional(),
176
+ };
177
+ const suiteIdentifier = {
178
+ suiteUuid: z.string().uuid().optional(),
179
+ suiteName: z.string().min(1).optional(),
180
+ };
181
+ export const CreateTestSuiteInputSchema = z.object({
182
+ name: z.string().min(1),
183
+ description: z.string().min(1),
184
+ ...projectIdentifier,
185
+ }).strict();
186
+ export const SearchTestSuitesInputSchema = z.object({
187
+ ...projectIdentifier,
188
+ search: z.string().optional(),
189
+ page: z.number().int().min(1).optional(),
190
+ pageSize: z.number().int().min(1).max(100).optional(),
191
+ }).strict();
192
+ export const DeleteTestSuiteInputSchema = z.object({
193
+ ...suiteIdentifier,
194
+ ...projectIdentifier,
195
+ }).strict();
196
+ export const CreateTestCaseInputSchema = z.object({
197
+ name: z.string().min(1),
198
+ description: z.string().min(1),
199
+ agentTaskDescription: z.string().min(1),
200
+ ...suiteIdentifier,
201
+ ...projectIdentifier,
202
+ relativeUrl: z.string().regex(/^\//, 'Must start with /').optional(),
203
+ maxSteps: z.number().int().min(1).max(100).optional(),
204
+ }).strict();
205
+ export const UpdateTestCaseInputSchema = z.object({
206
+ testUuid: z.string().uuid(),
207
+ name: z.string().min(1).optional(),
208
+ description: z.string().min(1).optional(),
209
+ agentTaskDescription: z.string().min(1).optional(),
210
+ }).strict();
211
+ export const DeleteTestCaseInputSchema = z.object({
212
+ testUuid: z.string().uuid(),
213
+ }).strict();
214
+ export const RunTestSuiteInputSchema = z.object({
215
+ ...suiteIdentifier,
216
+ ...projectIdentifier,
217
+ targetUrl: z.string().url().optional(),
218
+ }).strict();
219
+ export const GetTestSuiteResultsInputSchema = z.object({
220
+ ...suiteIdentifier,
221
+ ...projectIdentifier,
222
+ }).strict();
@@ -0,0 +1,27 @@
1
+ export async function resolveProject(client, name) {
2
+ const { projects } = await client.listProjects({ page: 1, pageSize: 100 }, name);
3
+ return resolveByName(name, projects, 'Project');
4
+ }
5
+ export async function resolveTestSuite(client, suiteName, projectUuid) {
6
+ const { suites } = await client.listTestSuites({ projectUuid, search: suiteName });
7
+ return resolveByName(suiteName, suites, 'TestSuite');
8
+ }
9
+ export function resolveByName(name, candidates, kind) {
10
+ const needle = name.toLowerCase();
11
+ const matches = candidates.filter(c => c.name.toLowerCase() === needle);
12
+ if (matches.length === 0) {
13
+ return {
14
+ error: `${kind}NotFound`,
15
+ message: `No ${kind.toLowerCase().replace('testsuite', 'test suite')} matching "${name}" found.` +
16
+ (candidates.length > 0 ? ` Available: ${candidates.slice(0, 10).map(c => `"${c.name}"`).join(', ')}` : ' (none accessible to this API key)'),
17
+ };
18
+ }
19
+ if (matches.length > 1) {
20
+ return {
21
+ error: 'AmbiguousMatch',
22
+ message: `Multiple ${kind.toLowerCase().replace('testsuite', 'test suite')}s match "${name}". Pass the uuid directly.`,
23
+ candidates: matches.map(m => ({ uuid: m.uuid, name: m.name })),
24
+ };
25
+ }
26
+ return { uuid: matches[0].uuid };
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {