@kirrosh/zond 0.11.0 → 0.12.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/package.json +1 -1
- package/src/core/generator/index.ts +1 -1
- package/src/core/generator/suite-generator.ts +66 -3
- package/src/core/runner/execute-run.ts +12 -0
- package/src/core/setup-api.ts +45 -15
- package/src/mcp/tools/generate-and-save.ts +12 -0
- package/src/mcp/tools/query-db.ts +22 -3
- package/src/mcp/tools/run-tests.ts +42 -4
- package/src/mcp/tools/setup-api.ts +1 -1
package/package.json
CHANGED
|
@@ -12,4 +12,4 @@ export { compressEndpointsWithSchemas, buildGenerationGuide } from "./guide-buil
|
|
|
12
12
|
export type { GuideOptions } from "./guide-builder.ts";
|
|
13
13
|
export type { EndpointWarning, WarningCode } from "./endpoint-warnings.ts";
|
|
14
14
|
export type { EndpointInfo, ResponseInfo, GenerateOptions, SecuritySchemeInfo, CrudGroup } from "./types.ts";
|
|
15
|
-
export { generateSuites, generateStep, detectCrudGroups, generateCrudSuite } from "./suite-generator.ts";
|
|
15
|
+
export { generateSuites, generateStep, detectCrudGroups, generateCrudSuite, findUnresolvedVars } from "./suite-generator.ts";
|
|
@@ -117,6 +117,14 @@ function getCaptureField(ep: EndpointInfo): string {
|
|
|
117
117
|
return "id";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
const AUTH_PATH_PATTERNS = /\/(auth|login|signin|signup|register|token|oauth)\b/i;
|
|
121
|
+
|
|
122
|
+
function isAuthEndpoint(ep: EndpointInfo): boolean {
|
|
123
|
+
if (AUTH_PATH_PATTERNS.test(ep.path)) return true;
|
|
124
|
+
if (ep.tags?.some(t => /^auth/i.test(t))) return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
// ──────────────────────────────────────────────
|
|
121
129
|
// Public API
|
|
122
130
|
// ──────────────────────────────────────────────
|
|
@@ -292,6 +300,32 @@ export function generateCrudSuite(
|
|
|
292
300
|
return suite;
|
|
293
301
|
}
|
|
294
302
|
|
|
303
|
+
/** Find unresolved template variables in a suite (excluding known globals, captured vars, and env keys) */
|
|
304
|
+
export function findUnresolvedVars(suite: RawSuite, envKeys?: Set<string>): string[] {
|
|
305
|
+
const KNOWN = new Set(["base_url", "auth_token", "api_key"]);
|
|
306
|
+
if (envKeys) for (const k of envKeys) KNOWN.add(k);
|
|
307
|
+
const captured = new Set<string>();
|
|
308
|
+
for (const step of suite.tests) {
|
|
309
|
+
if (step.expect?.body) {
|
|
310
|
+
for (const val of Object.values(step.expect.body)) {
|
|
311
|
+
if (val && typeof val === "object" && "capture" in val) captured.add((val as any).capture);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const vars = new Set<string>();
|
|
316
|
+
const scan = (obj: unknown) => {
|
|
317
|
+
if (typeof obj === "string") {
|
|
318
|
+
for (const m of obj.matchAll(/\{\{([^$}][^}]*)\}\}/g)) {
|
|
319
|
+
if (!KNOWN.has(m[1]) && !captured.has(m[1])) vars.add(m[1]);
|
|
320
|
+
}
|
|
321
|
+
} else if (obj && typeof obj === "object") {
|
|
322
|
+
for (const v of Object.values(obj)) scan(v);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
scan(suite);
|
|
326
|
+
return [...vars];
|
|
327
|
+
}
|
|
328
|
+
|
|
295
329
|
/** Main entry point: generate all suites from endpoints */
|
|
296
330
|
export function generateSuites(opts: {
|
|
297
331
|
endpoints: EndpointInfo[];
|
|
@@ -302,8 +336,12 @@ export function generateSuites(opts: {
|
|
|
302
336
|
// Filter deprecated
|
|
303
337
|
const active = endpoints.filter(ep => !ep.deprecated);
|
|
304
338
|
|
|
339
|
+
// Separate auth endpoints
|
|
340
|
+
const authEndpoints = active.filter(isAuthEndpoint);
|
|
341
|
+
const nonAuth = active.filter(ep => !isAuthEndpoint(ep));
|
|
342
|
+
|
|
305
343
|
// 1. Detect CRUD groups
|
|
306
|
-
const crudGroups = detectCrudGroups(
|
|
344
|
+
const crudGroups = detectCrudGroups(nonAuth);
|
|
307
345
|
|
|
308
346
|
// Collect endpoints consumed by CRUD groups
|
|
309
347
|
const crudEndpointKeys = new Set<string>();
|
|
@@ -315,8 +353,8 @@ export function generateSuites(opts: {
|
|
|
315
353
|
if (g.delete) crudEndpointKeys.add(`${g.delete.method.toUpperCase()} ${g.delete.path}`);
|
|
316
354
|
}
|
|
317
355
|
|
|
318
|
-
// Remaining endpoints (not in any CRUD group)
|
|
319
|
-
const remaining =
|
|
356
|
+
// Remaining endpoints (not in any CRUD group, not auth)
|
|
357
|
+
const remaining = nonAuth.filter(ep => !crudEndpointKeys.has(`${ep.method.toUpperCase()} ${ep.path}`));
|
|
320
358
|
|
|
321
359
|
const suites: RawSuite[] = [];
|
|
322
360
|
|
|
@@ -384,5 +422,30 @@ export function generateSuites(opts: {
|
|
|
384
422
|
suites.push(generateCrudSuite(group, securitySchemes));
|
|
385
423
|
}
|
|
386
424
|
|
|
425
|
+
// 4. Auth suite (separate — requires real credentials)
|
|
426
|
+
if (authEndpoints.length > 0) {
|
|
427
|
+
const tests = authEndpoints.map(ep => generateStep(ep, securitySchemes));
|
|
428
|
+
const headers = getSuiteHeaders(authEndpoints, securitySchemes);
|
|
429
|
+
|
|
430
|
+
const suite: RawSuite = {
|
|
431
|
+
name: "auth",
|
|
432
|
+
tags: ["auth"],
|
|
433
|
+
fileStem: "auth",
|
|
434
|
+
base_url: "{{base_url}}",
|
|
435
|
+
tests,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (headers) {
|
|
439
|
+
suite.headers = headers;
|
|
440
|
+
for (const t of tests) {
|
|
441
|
+
if (t.headers && JSON.stringify(t.headers) === JSON.stringify(headers)) {
|
|
442
|
+
delete (t as any).headers;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
suites.push(suite);
|
|
448
|
+
}
|
|
449
|
+
|
|
387
450
|
return suites;
|
|
388
451
|
}
|
|
@@ -17,6 +17,7 @@ export interface ExecuteRunOptions {
|
|
|
17
17
|
tag?: string[];
|
|
18
18
|
envVars?: Record<string, string>;
|
|
19
19
|
dryRun?: boolean;
|
|
20
|
+
rerunFilter?: Set<string>; // "suite_name::test_name" keys to rerun
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface ExecuteRunResult {
|
|
@@ -40,6 +41,17 @@ export async function executeRun(options: ExecuteRunOptions): Promise<ExecuteRun
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Rerun filter: keep only specific failed tests
|
|
45
|
+
if (options.rerunFilter && options.rerunFilter.size > 0) {
|
|
46
|
+
for (const suite of suites) {
|
|
47
|
+
suite.tests = suite.tests.filter(t => options.rerunFilter!.has(`${suite.name}::${t.name}`));
|
|
48
|
+
}
|
|
49
|
+
suites = suites.filter(s => s.tests.length > 0);
|
|
50
|
+
if (suites.length === 0) {
|
|
51
|
+
throw new Error("No matching tests found for rerun filter");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
// Safe mode: filter to GET-only tests
|
|
44
56
|
if (safe) {
|
|
45
57
|
for (const suite of suites) {
|
package/src/core/setup-api.ts
CHANGED
|
@@ -14,7 +14,7 @@ function toYaml(vars: Record<string, string>): string {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface SetupApiOptions {
|
|
17
|
-
name
|
|
17
|
+
name?: string;
|
|
18
18
|
spec?: string;
|
|
19
19
|
dir?: string;
|
|
20
20
|
envVars?: Record<string, string>;
|
|
@@ -29,13 +29,49 @@ export interface SetupApiResult {
|
|
|
29
29
|
testPath: string;
|
|
30
30
|
baseUrl: string;
|
|
31
31
|
specEndpoints: number;
|
|
32
|
+
pathParams?: Record<string, string>;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult> {
|
|
35
|
-
const {
|
|
36
|
+
const { spec, dbPath } = options;
|
|
36
37
|
|
|
37
38
|
getDb(dbPath);
|
|
38
39
|
|
|
40
|
+
// Try to load and validate spec, extract base_url
|
|
41
|
+
let openapiSpec: string | null = null;
|
|
42
|
+
let baseUrl = "";
|
|
43
|
+
let endpointCount = 0;
|
|
44
|
+
const pathParams = new Map<string, string>();
|
|
45
|
+
let specTitle: string | undefined;
|
|
46
|
+
if (spec) {
|
|
47
|
+
const doc = await readOpenApiSpec(spec);
|
|
48
|
+
openapiSpec = spec;
|
|
49
|
+
if ((doc as any).servers?.[0]?.url) {
|
|
50
|
+
baseUrl = (doc as any).servers[0].url;
|
|
51
|
+
}
|
|
52
|
+
specTitle = (doc as any).info?.title;
|
|
53
|
+
const endpoints = extractEndpoints(doc);
|
|
54
|
+
endpointCount = endpoints.length;
|
|
55
|
+
|
|
56
|
+
// Collect unique path parameters with default values
|
|
57
|
+
for (const ep of endpoints) {
|
|
58
|
+
for (const param of (ep.parameters ?? []).filter(p => p.in === "path")) {
|
|
59
|
+
if (pathParams.has(param.name)) continue;
|
|
60
|
+
const schema = param.schema as any;
|
|
61
|
+
if (param.example !== undefined) pathParams.set(param.name, String(param.example));
|
|
62
|
+
else if (schema?.example !== undefined) pathParams.set(param.name, String(schema.example));
|
|
63
|
+
else if (schema?.type === "integer" || schema?.type === "number") pathParams.set(param.name, "1");
|
|
64
|
+
else pathParams.set(param.name, "example");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Derive name: explicit > spec title > filename
|
|
70
|
+
const name = options.name
|
|
71
|
+
?? specTitle?.replace(/[^a-zA-Z0-9_\-\.]/g, "-").toLowerCase()
|
|
72
|
+
?? spec?.split(/[/\\]/).pop()?.replace(/\.\w+$/, "")
|
|
73
|
+
?? "api";
|
|
74
|
+
|
|
39
75
|
// Validate name uniqueness (or force-replace)
|
|
40
76
|
const existing = findCollectionByNameOrId(name);
|
|
41
77
|
if (existing) {
|
|
@@ -54,22 +90,13 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
|
|
|
54
90
|
// Create directories
|
|
55
91
|
mkdirSync(testPath, { recursive: true });
|
|
56
92
|
|
|
57
|
-
// Try to load and validate spec, extract base_url
|
|
58
|
-
let openapiSpec: string | null = null;
|
|
59
|
-
let baseUrl = "";
|
|
60
|
-
let endpointCount = 0;
|
|
61
|
-
if (spec) {
|
|
62
|
-
const doc = await readOpenApiSpec(spec);
|
|
63
|
-
openapiSpec = spec;
|
|
64
|
-
if ((doc as any).servers?.[0]?.url) {
|
|
65
|
-
baseUrl = (doc as any).servers[0].url;
|
|
66
|
-
}
|
|
67
|
-
endpointCount = extractEndpoints(doc).length;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
93
|
// Build environment variables
|
|
71
94
|
const envVars: Record<string, string> = {};
|
|
72
95
|
if (baseUrl) envVars.base_url = baseUrl;
|
|
96
|
+
// Add path parameter defaults (before user overrides)
|
|
97
|
+
for (const [k, v] of pathParams) {
|
|
98
|
+
if (!(k in envVars)) envVars[k] = v;
|
|
99
|
+
}
|
|
73
100
|
if (options.envVars) {
|
|
74
101
|
Object.assign(envVars, options.envVars);
|
|
75
102
|
}
|
|
@@ -102,6 +129,8 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
|
|
|
102
129
|
openapi_spec: openapiSpec ?? undefined,
|
|
103
130
|
});
|
|
104
131
|
|
|
132
|
+
const pathParamsObj = pathParams.size > 0 ? Object.fromEntries(pathParams) : undefined;
|
|
133
|
+
|
|
105
134
|
return {
|
|
106
135
|
created: true,
|
|
107
136
|
collectionId,
|
|
@@ -109,5 +138,6 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
|
|
|
109
138
|
testPath: normalizedTestPath,
|
|
110
139
|
baseUrl,
|
|
111
140
|
specEndpoints: endpointCount,
|
|
141
|
+
...(pathParamsObj ? { pathParams: pathParamsObj } : {}),
|
|
112
142
|
};
|
|
113
143
|
}
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
filterUncoveredEndpoints,
|
|
10
10
|
serializeSuite,
|
|
11
11
|
generateSuites,
|
|
12
|
+
findUnresolvedVars,
|
|
12
13
|
} from "../../core/generator/index.ts";
|
|
14
|
+
import { loadEnvironment } from "../../core/parser/variables.ts";
|
|
13
15
|
import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
|
|
14
16
|
import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
|
|
15
17
|
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
@@ -130,10 +132,20 @@ export function registerGenerateAndSaveTool(server: McpServer) {
|
|
|
130
132
|
});
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
const warnings: string[] = [];
|
|
136
|
+
const env = await loadEnvironment(undefined, effectiveOutputDir);
|
|
137
|
+
const envKeys = new Set(Object.keys(env));
|
|
138
|
+
for (const suite of suites) {
|
|
139
|
+
const unresolved = findUnresolvedVars(suite, envKeys);
|
|
140
|
+
if (unresolved.length > 0)
|
|
141
|
+
warnings.push(`${suite.fileStem ?? suite.name}.yaml: unresolved [${unresolved.join(", ")}]`);
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
const response: Record<string, unknown> = {
|
|
134
145
|
mode: "generate",
|
|
135
146
|
suitesGenerated: suites.length,
|
|
136
147
|
files,
|
|
148
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
137
149
|
hint: files.some(f => !f.saved)
|
|
138
150
|
? "Some files were not saved (already exist?). Use overwrite: true to replace."
|
|
139
151
|
: "Files saved. Run run_tests to verify. Use mode: 'guide' for LLM-crafted tests with more detail.",
|
|
@@ -16,6 +16,27 @@ function parseBodySafe(raw: string | null | undefined): unknown {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const USEFUL_HEADERS = new Set([
|
|
20
|
+
"content-type", "content-length", "location", "retry-after",
|
|
21
|
+
"www-authenticate", "allow",
|
|
22
|
+
]);
|
|
23
|
+
const USEFUL_PREFIXES = ["x-", "ratelimit"];
|
|
24
|
+
|
|
25
|
+
function filterHeaders(raw: string | null | undefined): Record<string, string> | undefined {
|
|
26
|
+
if (!raw) return undefined;
|
|
27
|
+
try {
|
|
28
|
+
const h = JSON.parse(raw) as Record<string, string>;
|
|
29
|
+
const out: Record<string, string> = {};
|
|
30
|
+
for (const [k, v] of Object.entries(h)) {
|
|
31
|
+
const l = k.toLowerCase();
|
|
32
|
+
if (USEFUL_HEADERS.has(l) || USEFUL_PREFIXES.some(p => l.startsWith(p))) {
|
|
33
|
+
out[k] = v;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
37
|
+
} catch { return undefined; }
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
export function registerQueryDbTool(server: McpServer, dbPath?: string) {
|
|
20
41
|
server.registerTool("query_db", {
|
|
21
42
|
description: TOOL_DESCRIPTIONS.query_db,
|
|
@@ -137,9 +158,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
|
|
|
137
158
|
...(hint ? { hint } : {}),
|
|
138
159
|
...(sHint ? { schema_hint: sHint } : {}),
|
|
139
160
|
response_body: parseBodySafe(r.response_body),
|
|
140
|
-
response_headers: r.response_headers
|
|
141
|
-
? JSON.parse(r.response_headers)
|
|
142
|
-
: undefined,
|
|
161
|
+
response_headers: filterHeaders(r.response_headers),
|
|
143
162
|
assertions: r.assertions,
|
|
144
163
|
duration_ms: r.duration_ms,
|
|
145
164
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { executeRun } from "../../core/runner/execute-run.ts";
|
|
5
|
+
import { getDb } from "../../db/schema.ts";
|
|
6
|
+
import { getResultsByRunId, findCollectionByTestPath } from "../../db/queries.ts";
|
|
7
|
+
import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints } from "../../core/generator/index.ts";
|
|
4
8
|
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
5
9
|
|
|
6
10
|
export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
@@ -13,8 +17,24 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
|
13
17
|
tag: z.optional(z.array(z.string())).describe("Filter suites by tag (OR logic)"),
|
|
14
18
|
envVars: z.optional(z.record(z.string(), z.string())).describe("Environment variables to inject (override env file, e.g. {\"TOKEN\": \"xxx\"})"),
|
|
15
19
|
dryRun: z.optional(z.boolean()).describe("Show requests without sending them (always exits 0)"),
|
|
20
|
+
rerunFrom: z.optional(z.number().int()).describe("Re-run only tests that failed/errored in this run ID"),
|
|
16
21
|
},
|
|
17
|
-
}, async ({ testPath, envName, safe, tag, envVars, dryRun }) => {
|
|
22
|
+
}, async ({ testPath, envName, safe, tag, envVars, dryRun, rerunFrom }) => {
|
|
23
|
+
// Build filter from previous failed run
|
|
24
|
+
let rerunFilter: Set<string> | undefined;
|
|
25
|
+
if (rerunFrom != null) {
|
|
26
|
+
getDb(dbPath);
|
|
27
|
+
const prevResults = getResultsByRunId(rerunFrom);
|
|
28
|
+
const failed = prevResults.filter(r => r.status === "fail" || r.status === "error");
|
|
29
|
+
if (failed.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: `Run ${rerunFrom} has no failures to rerun` }, null, 2) }],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
rerunFilter = new Set(failed.map(r => `${r.suite_name}::${r.test_name}`));
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
const { runId, results } = await executeRun({
|
|
19
39
|
testPath,
|
|
20
40
|
envName,
|
|
@@ -24,6 +44,7 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
|
24
44
|
tag,
|
|
25
45
|
envVars,
|
|
26
46
|
dryRun,
|
|
47
|
+
rerunFilter,
|
|
27
48
|
});
|
|
28
49
|
|
|
29
50
|
const total = results.reduce((s, r) => s + r.total, 0);
|
|
@@ -45,6 +66,25 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
|
45
66
|
}))
|
|
46
67
|
);
|
|
47
68
|
|
|
69
|
+
// Best-effort coverage calculation
|
|
70
|
+
let coverage: { covered: number; total: number; percentage: number } | undefined;
|
|
71
|
+
try {
|
|
72
|
+
const resolvedPath = resolve(testPath);
|
|
73
|
+
const collection = findCollectionByTestPath(resolvedPath);
|
|
74
|
+
if (collection?.openapi_spec) {
|
|
75
|
+
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
76
|
+
const allEndpoints = extractEndpoints(doc);
|
|
77
|
+
const coveredEps = await scanCoveredEndpoints(collection.test_path);
|
|
78
|
+
const uncovered = filterUncoveredEndpoints(allEndpoints, coveredEps);
|
|
79
|
+
const coveredCount = allEndpoints.length - uncovered.length;
|
|
80
|
+
coverage = {
|
|
81
|
+
covered: coveredCount,
|
|
82
|
+
total: allEndpoints.length,
|
|
83
|
+
percentage: allEndpoints.length > 0 ? Math.round((coveredCount / allEndpoints.length) * 100) : 100,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} catch { /* coverage is best-effort, don't fail run */ }
|
|
87
|
+
|
|
48
88
|
const hints: string[] = [];
|
|
49
89
|
if (failedSteps.length > 0) {
|
|
50
90
|
hints.push("Use query_db(action: 'diagnose_failure', runId: " + runId + ") for detailed failure analysis");
|
|
@@ -55,9 +95,6 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
|
55
95
|
);
|
|
56
96
|
}
|
|
57
97
|
}
|
|
58
|
-
hints.push("Use manage_server(action: 'start') to launch the Web UI and view results visually in a browser at http://localhost:8080");
|
|
59
|
-
hints.push("Ask the user if they want to set up CI/CD to run these tests automatically on push. If yes, use ci_init to generate a workflow and help them push to GitHub/GitLab.");
|
|
60
|
-
|
|
61
98
|
const summary = {
|
|
62
99
|
runId,
|
|
63
100
|
total,
|
|
@@ -67,6 +104,7 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
|
67
104
|
suites: results.length,
|
|
68
105
|
status: failed === 0 ? "all_passed" : "has_failures",
|
|
69
106
|
...(failedSteps.length > 0 ? { failures: failedSteps } : {}),
|
|
107
|
+
...(coverage ? { coverage } : {}),
|
|
70
108
|
hints,
|
|
71
109
|
};
|
|
72
110
|
|
|
@@ -22,7 +22,7 @@ export function registerSetupApiTool(server: McpServer, dbPath?: string) {
|
|
|
22
22
|
server.registerTool("setup_api", {
|
|
23
23
|
description: TOOL_DESCRIPTIONS.setup_api,
|
|
24
24
|
inputSchema: {
|
|
25
|
-
name: z.string().describe("API name (
|
|
25
|
+
name: z.optional(z.string()).describe("API name (auto-detected from spec title if omitted)"),
|
|
26
26
|
specPath: z.optional(z.string()).describe("Path or URL to OpenAPI spec"),
|
|
27
27
|
dir: z.optional(z.string()).describe("Base directory (default: ./apis/<name>/)"),
|
|
28
28
|
envVars: z.optional(z.string()).describe("Environment variables as JSON string (e.g. '{\"base_url\": \"...\", \"token\": \"...\"}')"),
|