@kirrosh/zond 0.7.1 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
@@ -1,15 +1,14 @@
1
- export const AGENT_SYSTEM_PROMPT = `You are an API testing assistant powered by zond. You help users run, create, validate, and diagnose API tests.
1
+ export const AGENT_SYSTEM_PROMPT = `You are an API testing assistant powered by zond. You help users run, create, and diagnose API tests.
2
2
 
3
3
  You have access to the following tools:
4
4
 
5
5
  - **run_tests**: Execute API test suites from YAML files or directories. Returns pass/fail summary with run ID.
6
- - **validate_tests**: Validate YAML test files without executing them. Check syntax and structure.
7
6
  - **query_results**: Query historical test run results and collections from the database.
8
7
  - **diagnose_failure**: Analyze a failed test run to identify root causes and suggest fixes.
8
+ - **send_request**: Send an ad-hoc HTTP request for quick testing.
9
9
 
10
10
  Tool usage examples:
11
11
  - run_tests: { testPath: "tests/api.yaml" } or { testPath: "tests/", envName: "staging", safe: true }
12
- - validate_tests: { testPath: "tests/api.yaml" }
13
12
  - query_results: action must be "list_runs", "get_run" (requires runId), or "list_collections"
14
13
  - List runs: { action: "list_runs", limit: 10 }
15
14
  - Get run details: { action: "get_run", runId: 1 }
@@ -1,10 +1,8 @@
1
1
  import { tool } from "ai";
2
2
  import { runTestsTool } from "./run-tests.ts";
3
- import { validateTestsTool } from "./validate-tests.ts";
4
3
  import { queryResultsTool } from "./query-results.ts";
5
4
  import { diagnoseFailureTool } from "./diagnose-failure.ts";
6
5
  import { sendRequestTool } from "./send-request.ts";
7
- import { exploreApiTool } from "./explore-api.ts";
8
6
  import type { AgentConfig } from "../types.ts";
9
7
 
10
8
  export function buildAgentTools(config: AgentConfig) {
@@ -35,12 +33,10 @@ export function buildAgentTools(config: AgentConfig) {
35
33
 
36
34
  return {
37
35
  run_tests,
38
- validate_tests: validateTestsTool,
39
36
  query_results: queryResultsTool,
40
37
  diagnose_failure: diagnoseFailureTool,
41
38
  send_request,
42
- explore_api: exploreApiTool,
43
39
  };
44
40
  }
45
41
 
46
- export { runTestsTool, validateTestsTool, queryResultsTool, diagnoseFailureTool, sendRequestTool, exploreApiTool };
42
+ export { runTestsTool, queryResultsTool, diagnoseFailureTool, sendRequestTool };
@@ -1,56 +1,5 @@
1
- import { z } from "zod";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../core/generator/index.ts";
4
- import type { EndpointInfo, SecuritySchemeInfo } from "../../core/generator/types.ts";
5
- import { compressSchema, formatParam, isAnySchema } from "../../core/generator/schema-utils.ts";
6
- import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
-
8
- export function registerGenerateTestsGuideTool(server: McpServer) {
9
- server.registerTool("generate_tests_guide", {
10
- description: TOOL_DESCRIPTIONS.generate_tests_guide,
11
- inputSchema: {
12
- specPath: z.string().describe("Path or URL to OpenAPI spec file"),
13
- outputDir: z.optional(z.string()).describe("Directory for saving test files (default: ./tests/)"),
14
- methodFilter: z.optional(z.array(z.string())).describe("Only include endpoints with these HTTP methods (e.g. [\"GET\"] for smoke tests)"),
15
- tag: z.optional(z.string()).describe("Filter endpoints by tag"),
16
- },
17
- }, async ({ specPath, outputDir, methodFilter, tag }) => {
18
- try {
19
- const doc = await readOpenApiSpec(specPath);
20
- let endpoints = extractEndpoints(doc);
21
- if (methodFilter && methodFilter.length > 0) {
22
- const methods = methodFilter.map(m => m.toUpperCase());
23
- endpoints = endpoints.filter(ep => methods.includes(ep.method.toUpperCase()));
24
- }
25
- if (tag) {
26
- const lower = tag.toLowerCase();
27
- endpoints = endpoints.filter(ep => ep.tags.some(t => t.toLowerCase() === lower));
28
- }
29
- const securitySchemes = extractSecuritySchemes(doc);
30
- const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
31
- const title = (doc as any).info?.title as string | undefined;
32
-
33
- const apiContext = compressEndpointsWithSchemas(endpoints, securitySchemes);
34
- const guide = buildGenerationGuide({
35
- title: title ?? "API",
36
- baseUrl,
37
- apiContext,
38
- outputDir: outputDir ?? "./tests/",
39
- securitySchemes,
40
- endpointCount: endpoints.length,
41
- });
42
-
43
- return {
44
- content: [{ type: "text" as const, text: guide }],
45
- };
46
- } catch (err) {
47
- return {
48
- content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
49
- isError: true,
50
- };
51
- }
52
- });
53
- }
1
+ import type { EndpointInfo, SecuritySchemeInfo } from "./types.ts";
2
+ import { compressSchema, formatParam, isAnySchema } from "./schema-utils.ts";
54
3
 
55
4
  export function compressEndpointsWithSchemas(
56
5
  endpoints: EndpointInfo[],
@@ -262,7 +211,7 @@ Never put actual key values directly in YAML test files.
262
211
  - Note required fields in request bodies
263
212
 
264
213
  ### Step 2: Plan Test Suites
265
- Before generating, check coverage with \`coverage_analysis\` to avoid duplicating existing tests. Use \`generate_missing_tests\` for incremental generation.
214
+ Before generating, check coverage with \`coverage_analysis\` to avoid duplicating existing tests. Use \`generate_and_save(testsDir=...)\` for incremental generation of uncovered endpoints only.
266
215
 
267
216
  > **Coverage note**: coverage is a static scan of YAML files — an endpoint is "covered" if a test file contains a matching METHOD + path line, regardless of whether tests pass or actually run.
268
217
 
@@ -372,13 +321,13 @@ Example: \`zond run --tag smoke --safe\` → reads-only, safe against production
372
321
  | Tool | When |
373
322
  |------|------|
374
323
  | \`setup_api\` | Register a new API (creates dirs, reads spec, sets up env) |
375
- | \`generate_tests_guide\` | Get this guide for full API spec |
376
- | \`generate_missing_tests\` | Get guide for only uncovered endpoints |
324
+ | \`generate_and_save\` | Get test generation guide (with auto-chunking for large APIs) |
377
325
  | \`save_test_suite\` | Save generated YAML (validates before writing) |
326
+ | \`save_test_suites\` | Save multiple YAML files in one call |
378
327
  | \`run_tests\` | Execute saved test suites |
379
328
  | \`query_db\` | Query runs, collections, results, diagnose failures |
380
329
  | \`coverage_analysis\` | Find untested endpoints for incremental generation |
381
- | \`explore_api\` | Re-check specific endpoints (use includeSchemas=true) |
330
+ | \`describe_endpoint\` | Get full details for one endpoint when debugging |
382
331
  | \`ci_init\` | Generate CI/CD workflow (GitHub Actions / GitLab CI) to run tests on push |
383
332
 
384
333
  ## Workflow After Tests Pass
@@ -8,5 +8,7 @@ export type { AIProviderConfig, AIGenerateOptions, AIGenerateResult } from "./ai
8
8
  export { scanCoveredEndpoints, filterUncoveredEndpoints, normalizePath, specPathToRegex } from "./coverage-scanner.ts";
9
9
  export type { CoveredEndpoint } from "./coverage-scanner.ts";
10
10
  export { analyzeEndpoints } from "./endpoint-warnings.ts";
11
+ export { compressEndpointsWithSchemas, buildGenerationGuide } from "./guide-builder.ts";
12
+ export type { GuideOptions } from "./guide-builder.ts";
11
13
  export type { EndpointWarning, WarningCode } from "./endpoint-warnings.ts";
12
14
  export type { EndpointInfo, ResponseInfo, GenerateOptions, SecuritySchemeInfo, CrudGroup } from "./types.ts";
@@ -1,5 +1,35 @@
1
1
  import type { OpenAPIV3 } from "openapi-types";
2
2
 
3
+ /**
4
+ * Deep-clone an object, replacing circular references with `{ "$ref": "[Circular]" }`.
5
+ * Uses WeakSet to track visited objects.
6
+ */
7
+ export function decycleSchema(obj: unknown): unknown {
8
+ const seen = new WeakSet<object>();
9
+
10
+ function walk(value: unknown): unknown {
11
+ if (value === null || typeof value !== "object") return value;
12
+
13
+ if (Array.isArray(value)) {
14
+ return value.map(item => walk(item));
15
+ }
16
+
17
+ const obj = value as Record<string, unknown>;
18
+ if (seen.has(obj)) {
19
+ return { $ref: "[Circular]" };
20
+ }
21
+ seen.add(obj);
22
+
23
+ const result: Record<string, unknown> = {};
24
+ for (const key of Object.keys(obj)) {
25
+ result[key] = walk(obj[key]);
26
+ }
27
+ return result;
28
+ }
29
+
30
+ return walk(obj);
31
+ }
32
+
3
33
  /**
4
34
  * Returns true if the schema is effectively `any` — no type, no properties, no constraints.
5
35
  */
@@ -14,42 +14,20 @@ export const TOOL_DESCRIPTIONS = {
14
14
  "sets up environment variables, and creates a collection in the database. " +
15
15
  "Use this before generating tests for a new API.",
16
16
 
17
- explore_api:
18
- "Explore an OpenAPI spec — list endpoints, servers, and security schemes. " +
19
- "Use with includeSchemas=true when generating tests to get full request/response body schemas.",
20
-
21
17
  describe_endpoint:
22
18
  "Full details for one endpoint: params grouped by type, request body schema, " +
23
19
  "all response schemas + response headers, security, deprecated flag. " +
24
20
  "Use when a test fails and you need complete endpoint spec without reading the whole file.",
25
21
 
26
- generate_tests_guide:
27
- "Get a comprehensive guide for generating API test suites. " +
28
- "Returns the full API specification (with request/response schemas) and a step-by-step algorithm " +
29
- "for creating YAML test files. Use this BEFORE generating tests — it gives you " +
30
- "everything you need to write high-quality test suites. " +
31
- "After generating, use save_test_suite to save, run_tests to execute, " +
32
- "manage_server(action: 'start') to view results in the Web UI, " +
33
- "and query_db(action: 'diagnose_failure') to debug failures.",
34
-
35
- generate_missing_tests:
36
- "Analyze test coverage and generate a test guide for only the uncovered endpoints. " +
37
- "Combines coverage_analysis + generate_tests_guide — returns a focused guide for missing tests. " +
38
- "Use this for incremental test generation to avoid duplicating existing tests. " +
39
- "After saving and running new tests, use manage_server(action: 'start') to view results in the Web UI.",
40
-
41
22
  save_test_suite:
42
23
  "Save a YAML test suite file with validation. Parses and validates the YAML content " +
43
24
  "before writing. Returns structured errors if validation fails so you can fix and retry. " +
44
- "Use after generating test content with generate_tests_guide.",
25
+ "Use after generating test content with generate_and_save.",
45
26
 
46
27
  save_test_suites:
47
28
  "Save multiple YAML test suite files in a single call. Each file is validated before writing. " +
48
29
  "Returns per-file results. Use when you have generated multiple suites at once.",
49
30
 
50
- validate_tests:
51
- "Validate YAML test files without running them. Returns parsed suite info or validation errors.",
52
-
53
31
  run_tests:
54
32
  "Execute API tests from a YAML file or directory and return results summary with failures. " +
55
33
  "Use after saving test suites with save_test_suite. Check query_db(action: 'diagnose_failure') for detailed failure analysis.",
package/src/mcp/server.ts CHANGED
@@ -1,15 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { registerRunTestsTool } from "./tools/run-tests.ts";
4
- import { registerValidateTestsTool } from "./tools/validate-tests.ts";
5
4
  import { registerQueryDbTool } from "./tools/query-db.ts";
6
5
  import { registerSendRequestTool } from "./tools/send-request.ts";
7
- import { registerExploreApiTool } from "./tools/explore-api.ts";
8
6
  import { registerCoverageAnalysisTool } from "./tools/coverage-analysis.ts";
9
7
  import { registerSaveTestSuiteTool, registerSaveTestSuitesTool } from "./tools/save-test-suite.ts";
10
- import { registerGenerateTestsGuideTool } from "./tools/generate-tests-guide.ts";
11
8
  import { registerSetupApiTool } from "./tools/setup-api.ts";
12
- import { registerGenerateMissingTestsTool } from "./tools/generate-missing-tests.ts";
13
9
  import { registerManageServerTool } from "./tools/manage-server.ts";
14
10
  import { registerCiInitTool } from "./tools/ci-init.ts";
15
11
  import { registerSetWorkDirTool } from "./tools/set-work-dir.ts";
@@ -25,21 +21,17 @@ export async function startMcpServer(options: McpServerOptions = {}): Promise<vo
25
21
 
26
22
  const server = new McpServer({
27
23
  name: "zond",
28
- version: "0.7.1",
24
+ version: "0.8.0",
29
25
  });
30
26
 
31
27
  // Register all tools
32
28
  registerRunTestsTool(server, dbPath);
33
- registerValidateTestsTool(server);
34
29
  registerQueryDbTool(server, dbPath);
35
30
  registerSendRequestTool(server, dbPath);
36
- registerExploreApiTool(server);
37
31
  registerCoverageAnalysisTool(server, dbPath);
38
32
  registerSaveTestSuiteTool(server, dbPath);
39
33
  registerSaveTestSuitesTool(server, dbPath);
40
- registerGenerateTestsGuideTool(server);
41
34
  registerSetupApiTool(server, dbPath);
42
- registerGenerateMissingTestsTool(server);
43
35
  registerManageServerTool(server, dbPath);
44
36
  registerCiInitTool(server);
45
37
  registerSetWorkDirTool(server);
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import type { OpenAPIV3 } from "openapi-types";
4
4
  import { readOpenApiSpec } from "../../core/generator/index.ts";
5
+ import { decycleSchema } from "../../core/generator/schema-utils.ts";
5
6
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
6
7
 
7
8
  function generateTestSnippet(params: {
@@ -229,7 +230,7 @@ export function registerDescribeEndpointTool(server: McpServer) {
229
230
  };
230
231
 
231
232
  return {
232
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
233
+ content: [{ type: "text" as const, text: JSON.stringify(decycleSchema(result), null, 2) }],
233
234
  };
234
235
  } catch (err) {
235
236
  return {
@@ -7,7 +7,7 @@ import {
7
7
  scanCoveredEndpoints,
8
8
  filterUncoveredEndpoints,
9
9
  } from "../../core/generator/index.ts";
10
- import { compressEndpointsWithSchemas, buildGenerationGuide } from "./generate-tests-guide.ts";
10
+ import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
11
11
  import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
12
12
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
13
13
 
@@ -138,7 +138,7 @@ export async function validateAndSave(
138
138
 
139
139
  const coverage: Record<string, unknown> = { percentage, covered: coveredCount, total, uncoveredCount: uncovered.length };
140
140
  if (percentage < 80 && uncovered.length > 0) {
141
- coverage.suggestion = `Use generate_missing_tests to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
141
+ coverage.suggestion = `Use generate_and_save with testsDir to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
142
142
  }
143
143
  result.coverage = coverage;
144
144
  }
@@ -8,7 +8,7 @@ import { basename } from "node:path";
8
8
 
9
9
  export function renderSuitesTab(state: CollectionState): string {
10
10
  if (state.suites.length === 0) {
11
- return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>generate_tests_guide</code> or <code>generate_and_save</code>.</div>`;
11
+ return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>generate_and_save</code>.</div>`;
12
12
  }
13
13
 
14
14
  const rows = state.suites.map((s, i) => renderSuiteRow(s, i)).join("");
@@ -1,40 +0,0 @@
1
- import { tool } from "ai";
2
- import { z } from "zod";
3
- import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../generator/index.ts";
4
-
5
- export const exploreApiTool = tool({
6
- description: "Explore an OpenAPI spec — list endpoints with method, path, and summary. Optionally filter by tag.",
7
- inputSchema: z.object({
8
- specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
9
- tag: z.string().optional().describe("Filter endpoints by tag"),
10
- }),
11
- execute: async (args) => {
12
- try {
13
- const doc = await readOpenApiSpec(args.specPath);
14
- const allEndpoints = extractEndpoints(doc);
15
- const securitySchemes = extractSecuritySchemes(doc);
16
- const servers = ((doc as any).servers ?? []) as Array<{ url: string }>;
17
-
18
- const endpoints = args.tag
19
- ? allEndpoints.filter(ep => ep.tags.includes(args.tag!))
20
- : allEndpoints;
21
-
22
- // Compact output — method + path + summary only
23
- return {
24
- title: (doc as any).info?.title,
25
- version: (doc as any).info?.version,
26
- servers: servers.map(s => s.url),
27
- securitySchemes: securitySchemes.map(s => s.name),
28
- totalEndpoints: allEndpoints.length,
29
- ...(args.tag ? { filteredByTag: args.tag, matchingEndpoints: endpoints.length } : {}),
30
- endpoints: endpoints.map(ep => ({
31
- method: ep.method,
32
- path: ep.path,
33
- summary: ep.summary,
34
- })),
35
- };
36
- } catch (err) {
37
- return { error: (err as Error).message };
38
- }
39
- },
40
- });
@@ -1,23 +0,0 @@
1
- import { tool } from "ai";
2
- import { z } from "zod";
3
- import { parse } from "../../parser/yaml-parser.ts";
4
-
5
- export const validateTestsTool = tool({
6
- description: "Validate YAML test files without running them. Returns parsed suite info or validation errors.",
7
- inputSchema: z.object({
8
- testPath: z.string().describe("Path to test YAML file or directory"),
9
- }),
10
- execute: async (args) => {
11
- try {
12
- const suites = await parse(args.testPath);
13
- return {
14
- valid: true,
15
- suiteCount: suites.length,
16
- totalTests: suites.reduce((s, suite) => s + suite.tests.length, 0),
17
- suites: suites.map((s) => ({ name: s.name, testCount: s.tests.length })),
18
- };
19
- } catch (err) {
20
- return { valid: false, error: (err as Error).message };
21
- }
22
- },
23
- });
@@ -1,84 +0,0 @@
1
- import { z } from "zod";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../core/generator/index.ts";
4
- import { compressSchema, formatParam } from "../../core/generator/schema-utils.ts";
5
- import { TOOL_DESCRIPTIONS } from "../descriptions.js";
6
-
7
- export function registerExploreApiTool(server: McpServer) {
8
- server.registerTool("explore_api", {
9
- description: TOOL_DESCRIPTIONS.explore_api,
10
- inputSchema: {
11
- specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
12
- tag: z.optional(z.string()).describe("Filter endpoints by tag"),
13
- includeSchemas: z.optional(z.boolean()).describe("Include request/response body schemas and parameter types (default: false)"),
14
- },
15
- }, async ({ specPath, tag, includeSchemas }) => {
16
- try {
17
- const doc = await readOpenApiSpec(specPath);
18
- const allEndpoints = extractEndpoints(doc);
19
- const securitySchemes = extractSecuritySchemes(doc);
20
- const servers = ((doc as any).servers ?? []) as Array<{ url: string; description?: string }>;
21
-
22
- const endpoints = tag
23
- ? allEndpoints.filter(ep => ep.tags.includes(tag))
24
- : allEndpoints;
25
-
26
- const result = {
27
- title: (doc as any).info?.title,
28
- version: (doc as any).info?.version,
29
- servers: servers.map(s => ({ url: s.url, description: s.description })),
30
- securitySchemes: securitySchemes.map(s => ({
31
- name: s.name,
32
- type: s.type,
33
- ...(s.scheme ? { scheme: s.scheme } : {}),
34
- ...(s.in ? { in: s.in, keyName: s.apiKeyName } : {}),
35
- })),
36
- totalEndpoints: allEndpoints.length,
37
- ...(tag ? { filteredByTag: tag, matchingEndpoints: endpoints.length } : {}),
38
- endpoints: endpoints.map(ep => {
39
- const base: Record<string, unknown> = {
40
- method: ep.method,
41
- path: ep.path,
42
- summary: ep.summary,
43
- tags: ep.tags,
44
- parameters: ep.parameters.map(p => ({
45
- name: p.name,
46
- in: p.in,
47
- required: p.required ?? false,
48
- ...(includeSchemas ? { type: formatParam(p).split(": ")[1] } : {}),
49
- })),
50
- hasRequestBody: !!ep.requestBodySchema,
51
- responses: ep.responses.map(r => ({
52
- statusCode: r.statusCode,
53
- description: r.description,
54
- ...(includeSchemas && r.schema ? { schema: compressSchema(r.schema) } : {}),
55
- })),
56
- };
57
-
58
- if (includeSchemas) {
59
- if (ep.requestBodySchema) {
60
- base.requestBodySchema = compressSchema(ep.requestBodySchema);
61
- }
62
- if (ep.requestBodyContentType) {
63
- base.requestBodyContentType = ep.requestBodyContentType;
64
- }
65
- if (ep.security.length > 0) {
66
- base.security = ep.security;
67
- }
68
- }
69
-
70
- return base;
71
- }),
72
- };
73
-
74
- return {
75
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
76
- };
77
- } catch (err) {
78
- return {
79
- content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
80
- isError: true,
81
- };
82
- }
83
- });
84
- }
@@ -1,91 +0,0 @@
1
- import { z } from "zod";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import {
4
- readOpenApiSpec,
5
- extractEndpoints,
6
- extractSecuritySchemes,
7
- scanCoveredEndpoints,
8
- filterUncoveredEndpoints,
9
- } from "../../core/generator/index.ts";
10
- import { compressEndpointsWithSchemas, buildGenerationGuide } from "./generate-tests-guide.ts";
11
- import { TOOL_DESCRIPTIONS } from "../descriptions.js";
12
-
13
- export function registerGenerateMissingTestsTool(server: McpServer) {
14
- server.registerTool("generate_missing_tests", {
15
- description: TOOL_DESCRIPTIONS.generate_missing_tests,
16
- inputSchema: {
17
- specPath: z.string().describe("Path or URL to OpenAPI spec file"),
18
- testsDir: z.string().describe("Path to directory with existing test YAML files"),
19
- outputDir: z.optional(z.string()).describe("Directory for saving new test files (default: same as testsDir)"),
20
- methodFilter: z.optional(z.array(z.string())).describe("Only include endpoints with these HTTP methods (e.g. [\"GET\"] for smoke tests)"),
21
- tag: z.optional(z.string()).describe("Filter endpoints by tag"),
22
- },
23
- }, async ({ specPath, testsDir, outputDir, methodFilter, tag }) => {
24
- try {
25
- const doc = await readOpenApiSpec(specPath);
26
- let allEndpoints = extractEndpoints(doc);
27
- const securitySchemes = extractSecuritySchemes(doc);
28
- const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
29
- const title = (doc as any).info?.title as string | undefined;
30
-
31
- // Apply method filter before coverage check
32
- if (methodFilter && methodFilter.length > 0) {
33
- const methods = methodFilter.map(m => m.toUpperCase());
34
- allEndpoints = allEndpoints.filter(ep => methods.includes(ep.method.toUpperCase()));
35
- }
36
- if (tag) {
37
- const lower = tag.toLowerCase();
38
- allEndpoints = allEndpoints.filter(ep => ep.tags.some(t => t.toLowerCase() === lower));
39
- }
40
-
41
- if (allEndpoints.length === 0) {
42
- return {
43
- content: [{ type: "text" as const, text: JSON.stringify({ error: "No endpoints found in the spec" }, null, 2) }],
44
- isError: true,
45
- };
46
- }
47
-
48
- const covered = await scanCoveredEndpoints(testsDir);
49
- const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
50
- const coveredCount = allEndpoints.length - uncovered.length;
51
- const percentage = Math.round((coveredCount / allEndpoints.length) * 100);
52
-
53
- if (uncovered.length === 0) {
54
- return {
55
- content: [{
56
- type: "text" as const,
57
- text: JSON.stringify({
58
- fullyCovered: true,
59
- percentage: 100,
60
- totalEndpoints: allEndpoints.length,
61
- covered: coveredCount,
62
- }, null, 2),
63
- }],
64
- };
65
- }
66
-
67
- // Build guide for uncovered endpoints only
68
- const apiContext = compressEndpointsWithSchemas(uncovered, securitySchemes);
69
- const coverageHeader = `## Coverage: ${coveredCount}/${allEndpoints.length} endpoints covered (${percentage}%). Generating tests for ${uncovered.length} uncovered endpoints:`;
70
-
71
- const guide = buildGenerationGuide({
72
- title: title ?? "API",
73
- baseUrl,
74
- apiContext,
75
- outputDir: outputDir ?? testsDir,
76
- securitySchemes,
77
- endpointCount: uncovered.length,
78
- coverageHeader,
79
- });
80
-
81
- return {
82
- content: [{ type: "text" as const, text: guide }],
83
- };
84
- } catch (err) {
85
- return {
86
- content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
87
- isError: true,
88
- };
89
- }
90
- });
91
- }
@@ -1,43 +0,0 @@
1
- import { z } from "zod";
2
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { parse } from "../../core/parser/yaml-parser.ts";
4
- import { TOOL_DESCRIPTIONS } from "../descriptions.js";
5
-
6
- export function registerValidateTestsTool(server: McpServer) {
7
- server.registerTool("validate_tests", {
8
- description: TOOL_DESCRIPTIONS.validate_tests,
9
- inputSchema: {
10
- testPath: z.string().describe("Path to test YAML file or directory"),
11
- },
12
- }, async ({ testPath }) => {
13
- try {
14
- const suites = await parse(testPath);
15
- const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
16
-
17
- const result = {
18
- valid: true,
19
- suites: suites.length,
20
- tests: totalSteps,
21
- details: suites.map(s => ({
22
- name: s.name,
23
- tests: s.tests.length,
24
- tags: s.tags,
25
- description: s.description,
26
- source: (s as any)._source,
27
- })),
28
- };
29
-
30
- return {
31
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
32
- };
33
- } catch (err) {
34
- return {
35
- content: [{ type: "text" as const, text: JSON.stringify({
36
- valid: false,
37
- error: (err as Error).message,
38
- }, null, 2) }],
39
- isError: true,
40
- };
41
- }
42
- });
43
- }