@kirrosh/zond 0.7.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/package.json +53 -0
  5. package/src/bun-types.d.ts +5 -0
  6. package/src/cli/commands/add-api.ts +51 -0
  7. package/src/cli/commands/ai-generate.ts +106 -0
  8. package/src/cli/commands/chat.ts +43 -0
  9. package/src/cli/commands/ci-init.ts +163 -0
  10. package/src/cli/commands/collections.ts +41 -0
  11. package/src/cli/commands/compare.ts +129 -0
  12. package/src/cli/commands/coverage.ts +156 -0
  13. package/src/cli/commands/doctor.ts +127 -0
  14. package/src/cli/commands/init.ts +84 -0
  15. package/src/cli/commands/mcp.ts +16 -0
  16. package/src/cli/commands/run.ts +156 -0
  17. package/src/cli/commands/runs.ts +108 -0
  18. package/src/cli/commands/serve.ts +22 -0
  19. package/src/cli/commands/update.ts +142 -0
  20. package/src/cli/commands/validate.ts +18 -0
  21. package/src/cli/index.ts +529 -0
  22. package/src/cli/output.ts +24 -0
  23. package/src/cli/runtime.ts +7 -0
  24. package/src/core/agent/agent-loop.ts +116 -0
  25. package/src/core/agent/context-manager.ts +41 -0
  26. package/src/core/agent/system-prompt.ts +28 -0
  27. package/src/core/agent/tools/diagnose-failure.ts +51 -0
  28. package/src/core/agent/tools/explore-api.ts +40 -0
  29. package/src/core/agent/tools/index.ts +46 -0
  30. package/src/core/agent/tools/query-results.ts +40 -0
  31. package/src/core/agent/tools/run-tests.ts +38 -0
  32. package/src/core/agent/tools/send-request.ts +44 -0
  33. package/src/core/agent/tools/validate-tests.ts +23 -0
  34. package/src/core/agent/types.ts +22 -0
  35. package/src/core/diagnostics/failure-hints.ts +63 -0
  36. package/src/core/generator/ai/ai-generator.ts +61 -0
  37. package/src/core/generator/ai/llm-client.ts +159 -0
  38. package/src/core/generator/ai/output-parser.ts +307 -0
  39. package/src/core/generator/ai/prompt-builder.ts +153 -0
  40. package/src/core/generator/ai/types.ts +56 -0
  41. package/src/core/generator/chunker.ts +47 -0
  42. package/src/core/generator/coverage-scanner.ts +87 -0
  43. package/src/core/generator/data-factory.ts +115 -0
  44. package/src/core/generator/endpoint-warnings.ts +43 -0
  45. package/src/core/generator/index.ts +12 -0
  46. package/src/core/generator/openapi-reader.ts +143 -0
  47. package/src/core/generator/schema-utils.ts +52 -0
  48. package/src/core/generator/serializer.ts +189 -0
  49. package/src/core/generator/types.ts +48 -0
  50. package/src/core/parser/filter.ts +14 -0
  51. package/src/core/parser/index.ts +21 -0
  52. package/src/core/parser/schema.ts +175 -0
  53. package/src/core/parser/types.ts +52 -0
  54. package/src/core/parser/variables.ts +154 -0
  55. package/src/core/parser/yaml-parser.ts +85 -0
  56. package/src/core/reporter/console.ts +175 -0
  57. package/src/core/reporter/index.ts +23 -0
  58. package/src/core/reporter/json.ts +9 -0
  59. package/src/core/reporter/junit.ts +78 -0
  60. package/src/core/reporter/types.ts +12 -0
  61. package/src/core/runner/assertions.ts +173 -0
  62. package/src/core/runner/execute-run.ts +97 -0
  63. package/src/core/runner/executor.ts +183 -0
  64. package/src/core/runner/http-client.ts +69 -0
  65. package/src/core/runner/index.ts +12 -0
  66. package/src/core/runner/types.ts +48 -0
  67. package/src/core/setup-api.ts +113 -0
  68. package/src/core/utils.ts +9 -0
  69. package/src/db/queries.ts +774 -0
  70. package/src/db/schema.ts +159 -0
  71. package/src/mcp/descriptions.ts +88 -0
  72. package/src/mcp/server.ts +52 -0
  73. package/src/mcp/tools/ci-init.ts +54 -0
  74. package/src/mcp/tools/coverage-analysis.ts +141 -0
  75. package/src/mcp/tools/describe-endpoint.ts +241 -0
  76. package/src/mcp/tools/explore-api.ts +84 -0
  77. package/src/mcp/tools/generate-and-save.ts +129 -0
  78. package/src/mcp/tools/generate-missing-tests.ts +91 -0
  79. package/src/mcp/tools/generate-tests-guide.ts +391 -0
  80. package/src/mcp/tools/manage-server.ts +86 -0
  81. package/src/mcp/tools/query-db.ts +255 -0
  82. package/src/mcp/tools/run-tests.ts +71 -0
  83. package/src/mcp/tools/save-test-suite.ts +218 -0
  84. package/src/mcp/tools/send-request.ts +63 -0
  85. package/src/mcp/tools/set-work-dir.ts +35 -0
  86. package/src/mcp/tools/setup-api.ts +84 -0
  87. package/src/mcp/tools/validate-tests.ts +43 -0
  88. package/src/tui/chat-ui.ts +150 -0
  89. package/src/web/data/collection-state.ts +360 -0
  90. package/src/web/routes/api.ts +234 -0
  91. package/src/web/routes/dashboard.ts +313 -0
  92. package/src/web/routes/runs.ts +64 -0
  93. package/src/web/schemas.ts +121 -0
  94. package/src/web/server.ts +134 -0
  95. package/src/web/static/htmx.min.js +1 -0
  96. package/src/web/static/style.css +827 -0
  97. package/src/web/views/endpoints-tab.ts +170 -0
  98. package/src/web/views/health-strip.ts +92 -0
  99. package/src/web/views/layout.ts +48 -0
  100. package/src/web/views/results.ts +209 -0
  101. package/src/web/views/runs-tab.ts +126 -0
  102. package/src/web/views/suites-tab.ts +153 -0
@@ -0,0 +1,159 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { resolve } from "path";
3
+ import { existsSync } from "fs";
4
+
5
+ let _db: Database | null = null;
6
+ let _dbPath: string | null = null;
7
+
8
+ export function getDb(dbPath?: string): Database {
9
+ const path = dbPath ? resolve(dbPath) : (_dbPath ?? resolve(process.cwd(), "zond.db"));
10
+
11
+ // If cached connection exists, verify the file still exists
12
+ if (_db && _dbPath === path && existsSync(path)) return _db;
13
+
14
+ // Close stale connection if any
15
+ if (_db) {
16
+ try { _db.close(); } catch {}
17
+ _db = null;
18
+ _dbPath = null;
19
+ }
20
+ const db = new Database(path, { create: true });
21
+
22
+ // Performance and integrity settings
23
+ db.exec("PRAGMA journal_mode = WAL");
24
+ db.exec("PRAGMA foreign_keys = ON");
25
+
26
+ runMigrations(db);
27
+
28
+ _db = db;
29
+ _dbPath = path;
30
+ return db;
31
+ }
32
+
33
+ export function closeDb(): void {
34
+ if (_db) {
35
+ try { _db.close(); } catch {}
36
+ _db = null;
37
+ _dbPath = null;
38
+ }
39
+ }
40
+
41
+ export function resetDb(): void {
42
+ if (_db) { try { _db.close(); } catch {} }
43
+ _db = null;
44
+ _dbPath = null;
45
+ }
46
+
47
+ // ──────────────────────────────────────────────
48
+ // Schema
49
+ // ──────────────────────────────────────────────
50
+
51
+ const SCHEMA_VERSION = 1;
52
+
53
+ const SCHEMA = `
54
+ CREATE TABLE IF NOT EXISTS runs (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ started_at TEXT NOT NULL,
57
+ finished_at TEXT,
58
+ total INTEGER NOT NULL DEFAULT 0,
59
+ passed INTEGER NOT NULL DEFAULT 0,
60
+ failed INTEGER NOT NULL DEFAULT 0,
61
+ skipped INTEGER NOT NULL DEFAULT 0,
62
+ trigger TEXT DEFAULT 'manual',
63
+ commit_sha TEXT,
64
+ branch TEXT,
65
+ environment TEXT,
66
+ duration_ms INTEGER,
67
+ collection_id INTEGER REFERENCES collections(id)
68
+ );
69
+
70
+ CREATE TABLE IF NOT EXISTS results (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ run_id INTEGER NOT NULL REFERENCES runs(id),
73
+ suite_name TEXT NOT NULL,
74
+ test_name TEXT NOT NULL,
75
+ status TEXT NOT NULL,
76
+ duration_ms INTEGER NOT NULL,
77
+ request_method TEXT,
78
+ request_url TEXT,
79
+ request_body TEXT,
80
+ response_status INTEGER,
81
+ response_body TEXT,
82
+ error_message TEXT,
83
+ assertions TEXT,
84
+ captures TEXT,
85
+ response_headers TEXT
86
+ );
87
+
88
+ CREATE TABLE IF NOT EXISTS collections (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ name TEXT NOT NULL,
91
+ test_path TEXT NOT NULL,
92
+ openapi_spec TEXT,
93
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
94
+ base_dir TEXT
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS ai_generations (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ collection_id INTEGER REFERENCES collections(id),
100
+ prompt TEXT NOT NULL,
101
+ model TEXT NOT NULL,
102
+ provider TEXT NOT NULL,
103
+ generated_yaml TEXT,
104
+ output_path TEXT,
105
+ status TEXT NOT NULL DEFAULT 'pending',
106
+ error_message TEXT,
107
+ prompt_tokens INTEGER,
108
+ completion_tokens INTEGER,
109
+ duration_ms INTEGER,
110
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
111
+ );
112
+
113
+ CREATE TABLE IF NOT EXISTS chat_sessions (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ title TEXT,
116
+ provider TEXT NOT NULL,
117
+ model TEXT NOT NULL,
118
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
119
+ last_active TEXT NOT NULL DEFAULT (datetime('now'))
120
+ );
121
+
122
+ CREATE TABLE IF NOT EXISTS chat_messages (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ session_id INTEGER NOT NULL REFERENCES chat_sessions(id),
125
+ role TEXT NOT NULL,
126
+ content TEXT NOT NULL,
127
+ tool_name TEXT,
128
+ tool_args TEXT,
129
+ tool_result TEXT,
130
+ input_tokens INTEGER,
131
+ output_tokens INTEGER,
132
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS settings (
136
+ key TEXT PRIMARY KEY,
137
+ value TEXT NOT NULL
138
+ );
139
+
140
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
141
+ CREATE INDEX IF NOT EXISTS idx_runs_collection ON runs(collection_id);
142
+ CREATE INDEX IF NOT EXISTS idx_results_run ON results(run_id);
143
+ CREATE INDEX IF NOT EXISTS idx_results_status ON results(status);
144
+ CREATE INDEX IF NOT EXISTS idx_results_name ON results(suite_name, test_name);
145
+ CREATE INDEX IF NOT EXISTS idx_collections_name ON collections(name);
146
+ CREATE INDEX IF NOT EXISTS idx_ai_gen_collection ON ai_generations(collection_id);
147
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
148
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_active ON chat_sessions(last_active DESC);
149
+ `;
150
+
151
+ function runMigrations(db: Database): void {
152
+ const ver = (db.query("PRAGMA user_version").get() as { user_version: number }).user_version;
153
+ if (ver >= SCHEMA_VERSION) return;
154
+
155
+ db.transaction(() => {
156
+ db.exec(SCHEMA);
157
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
158
+ })();
159
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Single source of truth for all MCP tool descriptions.
3
+ * Update descriptions here — they are imported by each tool file.
4
+ */
5
+ export const TOOL_DESCRIPTIONS = {
6
+ set_work_dir:
7
+ "Set the working directory for this MCP session. " +
8
+ "Call this FIRST before any other tool when using a shared MCP server (npx). " +
9
+ "Determines where zond.db and relative test paths resolve to. " +
10
+ "Pass the absolute path to your project root (same as workspace root in your editor).",
11
+
12
+ setup_api:
13
+ "Register a new API for testing. Creates directory structure, reads OpenAPI spec, " +
14
+ "sets up environment variables, and creates a collection in the database. " +
15
+ "Use this before generating tests for a new API.",
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
+ describe_endpoint:
22
+ "Full details for one endpoint: params grouped by type, request body schema, " +
23
+ "all response schemas + response headers, security, deprecated flag. " +
24
+ "Use when a test fails and you need complete endpoint spec without reading the whole file.",
25
+
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
+ save_test_suite:
42
+ "Save a YAML test suite file with validation. Parses and validates the YAML content " +
43
+ "before writing. Returns structured errors if validation fails so you can fix and retry. " +
44
+ "Use after generating test content with generate_tests_guide.",
45
+
46
+ save_test_suites:
47
+ "Save multiple YAML test suite files in a single call. Each file is validated before writing. " +
48
+ "Returns per-file results. Use when you have generated multiple suites at once.",
49
+
50
+ validate_tests:
51
+ "Validate YAML test files without running them. Returns parsed suite info or validation errors.",
52
+
53
+ run_tests:
54
+ "Execute API tests from a YAML file or directory and return results summary with failures. " +
55
+ "Use after saving test suites with save_test_suite. Check query_db(action: 'diagnose_failure') for detailed failure analysis.",
56
+
57
+ query_db:
58
+ "Query the zond database. Actions: list_collections (all APIs with run stats), " +
59
+ "list_runs (recent test runs), get_run_results (full detail for a run), " +
60
+ "diagnose_failure (only failed/errored steps for a run — each failure includes failure_type: api_error/assertion_failed/network_error, " +
61
+ "and summary includes api_errors/assertion_failures/network_errors counts), " +
62
+ "compare_runs (regressions and fixes between two runs).",
63
+
64
+ coverage_analysis:
65
+ "Compare an OpenAPI spec against existing test files to find untested endpoints. " +
66
+ "Use to identify gaps and prioritize which endpoints to generate tests for next. " +
67
+ "Pass runId to get enriched pass/fail/5xx breakdown per endpoint. " +
68
+ "Always includes static spec warnings (deprecated, missing response schemas, required params without examples).",
69
+
70
+ send_request:
71
+ "Send an ad-hoc HTTP request. Supports variable interpolation from environments (e.g. {{base_url}}).",
72
+
73
+ manage_server:
74
+ "Start, stop, restart, or check status of the zond WebUI server. " +
75
+ "Useful for viewing test results in a browser without leaving the MCP session.",
76
+
77
+ generate_and_save:
78
+ "Read an OpenAPI spec, auto-chunk by tags if large (>30 endpoints), " +
79
+ "and return a focused test generation guide. For large APIs returns a chunking plan — " +
80
+ "call again with tag parameter for each chunk. Use testsDir param to only generate for uncovered endpoints. " +
81
+ "After generating YAML, use save_test_suites to save files, then run_tests to verify.",
82
+
83
+ ci_init:
84
+ "Generate a CI/CD workflow file for running API tests automatically on push, PR, and schedule. " +
85
+ "Supports GitHub Actions and GitLab CI. Auto-detects platform from project structure " +
86
+ "(.github/ → GitHub, .gitlab-ci.yml → GitLab). " +
87
+ "Use after tests are generated and passing. After generating the workflow, help the user commit and push to activate CI.",
88
+ } as const;
@@ -0,0 +1,52 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { registerRunTestsTool } from "./tools/run-tests.ts";
4
+ import { registerValidateTestsTool } from "./tools/validate-tests.ts";
5
+ import { registerQueryDbTool } from "./tools/query-db.ts";
6
+ import { registerSendRequestTool } from "./tools/send-request.ts";
7
+ import { registerExploreApiTool } from "./tools/explore-api.ts";
8
+ import { registerCoverageAnalysisTool } from "./tools/coverage-analysis.ts";
9
+ import { registerSaveTestSuiteTool, registerSaveTestSuitesTool } from "./tools/save-test-suite.ts";
10
+ import { registerGenerateTestsGuideTool } from "./tools/generate-tests-guide.ts";
11
+ import { registerSetupApiTool } from "./tools/setup-api.ts";
12
+ import { registerGenerateMissingTestsTool } from "./tools/generate-missing-tests.ts";
13
+ import { registerManageServerTool } from "./tools/manage-server.ts";
14
+ import { registerCiInitTool } from "./tools/ci-init.ts";
15
+ import { registerSetWorkDirTool } from "./tools/set-work-dir.ts";
16
+ import { registerDescribeEndpointTool } from "./tools/describe-endpoint.ts";
17
+ import { registerGenerateAndSaveTool } from "./tools/generate-and-save.ts";
18
+
19
+ export interface McpServerOptions {
20
+ dbPath?: string;
21
+ }
22
+
23
+ export async function startMcpServer(options: McpServerOptions = {}): Promise<void> {
24
+ const { dbPath } = options;
25
+
26
+ const server = new McpServer({
27
+ name: "zond",
28
+ version: "0.4.0",
29
+ });
30
+
31
+ // Register all tools
32
+ registerRunTestsTool(server, dbPath);
33
+ registerValidateTestsTool(server);
34
+ registerQueryDbTool(server, dbPath);
35
+ registerSendRequestTool(server, dbPath);
36
+ registerExploreApiTool(server);
37
+ registerCoverageAnalysisTool(server, dbPath);
38
+ registerSaveTestSuiteTool(server, dbPath);
39
+ registerSaveTestSuitesTool(server, dbPath);
40
+ registerGenerateTestsGuideTool(server);
41
+ registerSetupApiTool(server, dbPath);
42
+ registerGenerateMissingTestsTool(server);
43
+ registerManageServerTool(server, dbPath);
44
+ registerCiInitTool(server);
45
+ registerSetWorkDirTool(server);
46
+ registerDescribeEndpointTool(server);
47
+ registerGenerateAndSaveTool(server);
48
+
49
+ // Connect via stdio transport
50
+ const transport = new StdioServerTransport();
51
+ await server.connect(transport);
52
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { ciInitCommand } from "../../cli/commands/ci-init.ts";
4
+ import { TOOL_DESCRIPTIONS } from "../descriptions.js";
5
+
6
+ export function registerCiInitTool(server: McpServer) {
7
+ server.registerTool("ci_init", {
8
+ description: TOOL_DESCRIPTIONS.ci_init,
9
+ inputSchema: {
10
+ platform: z.optional(z.enum(["github", "gitlab"]))
11
+ .describe("CI platform. If omitted, auto-detects from project structure (defaults to GitHub)"),
12
+ force: z.optional(z.boolean())
13
+ .describe("Overwrite existing CI config (default: false)"),
14
+ dir: z.optional(z.string())
15
+ .describe("Project root directory where CI config will be created (default: current working directory)"),
16
+ },
17
+ }, async ({ platform, force, dir }) => {
18
+ // Capture stdout to return as result
19
+ const logs: string[] = [];
20
+ const origWrite = process.stdout.write;
21
+ process.stdout.write = ((chunk: string | Uint8Array) => {
22
+ logs.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
23
+ return true;
24
+ }) as typeof process.stdout.write;
25
+
26
+ try {
27
+ const code = await ciInitCommand({
28
+ platform,
29
+ force: force ?? false,
30
+ dir,
31
+ });
32
+
33
+ process.stdout.write = origWrite;
34
+
35
+ const output = logs.join("").trim();
36
+ if (code !== 0) {
37
+ return {
38
+ content: [{ type: "text" as const, text: JSON.stringify({ error: output || "ci init failed", exitCode: code }, null, 2) }],
39
+ isError: true,
40
+ };
41
+ }
42
+
43
+ return {
44
+ content: [{ type: "text" as const, text: JSON.stringify({ message: output, exitCode: 0 }, null, 2) }],
45
+ };
46
+ } catch (err) {
47
+ process.stdout.write = origWrite;
48
+ return {
49
+ content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
50
+ isError: true,
51
+ };
52
+ }
53
+ });
54
+ }
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints, normalizePath, specPathToRegex, analyzeEndpoints } from "../../core/generator/index.ts";
4
+ import { getDb } from "../../db/schema.ts";
5
+ import { getResultsByRunId, getRunById } from "../../db/queries.ts";
6
+ import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
+
8
+ function extractPathFromUrl(url: string): string | null {
9
+ try {
10
+ return new URL(url).pathname;
11
+ } catch {
12
+ // If not a full URL, treat as path directly
13
+ return url.startsWith("/") ? url : null;
14
+ }
15
+ }
16
+
17
+ export function registerCoverageAnalysisTool(server: McpServer, dbPath?: string) {
18
+ server.registerTool("coverage_analysis", {
19
+ description: TOOL_DESCRIPTIONS.coverage_analysis,
20
+ inputSchema: {
21
+ specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
22
+ testsDir: z.string().describe("Path to directory with test YAML files"),
23
+ failThreshold: z.optional(z.number().min(0).max(100)).describe("Return isError when coverage % is below this threshold (0–100)"),
24
+ runId: z.optional(z.number().int()).describe("Run ID to cross-reference test results for pass/fail/5xx breakdown"),
25
+ },
26
+ }, async ({ specPath, testsDir, failThreshold, runId }) => {
27
+ try {
28
+ const doc = await readOpenApiSpec(specPath);
29
+ const allEndpoints = extractEndpoints(doc);
30
+
31
+ if (allEndpoints.length === 0) {
32
+ return {
33
+ content: [{ type: "text" as const, text: JSON.stringify({ error: "No endpoints found in the spec" }, null, 2) }],
34
+ isError: true,
35
+ };
36
+ }
37
+
38
+ const covered = await scanCoveredEndpoints(testsDir);
39
+ const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
40
+ const coveredCount = allEndpoints.length - uncovered.length;
41
+ const percentage = Math.round((coveredCount / allEndpoints.length) * 100);
42
+
43
+ // Static warnings
44
+ const warnings = analyzeEndpoints(allEndpoints);
45
+
46
+ const result: Record<string, unknown> = {
47
+ totalEndpoints: allEndpoints.length,
48
+ covered: coveredCount,
49
+ uncovered: uncovered.length,
50
+ percentage,
51
+ uncoveredEndpoints: uncovered.map(ep => ({
52
+ method: ep.method,
53
+ path: ep.path,
54
+ summary: ep.summary,
55
+ tags: ep.tags,
56
+ })),
57
+ coveredEndpoints: covered.map(ep => ({
58
+ method: ep.method,
59
+ path: ep.path,
60
+ file: ep.file,
61
+ })),
62
+ };
63
+
64
+ if (warnings.length > 0) {
65
+ result.warnings = warnings;
66
+ }
67
+
68
+ // Enriched breakdown when runId is provided
69
+ if (runId != null) {
70
+ getDb(dbPath);
71
+ const run = getRunById(runId);
72
+ if (!run) {
73
+ return {
74
+ content: [{ type: "text" as const, text: JSON.stringify({ error: `Run ${runId} not found` }, null, 2) }],
75
+ isError: true,
76
+ };
77
+ }
78
+
79
+ const results = getResultsByRunId(runId);
80
+
81
+ // Build a map: spec endpoint → status classification
82
+ const endpointStatus = new Map<string, "passing" | "api_error" | "test_failed">();
83
+
84
+ for (const r of results) {
85
+ if (!r.request_url || !r.request_method) continue;
86
+ const urlPath = extractPathFromUrl(r.request_url);
87
+ if (!urlPath) continue;
88
+ const normalizedUrl = normalizePath(urlPath);
89
+
90
+ // Find matching spec endpoint
91
+ for (const ep of allEndpoints) {
92
+ const regex = specPathToRegex(ep.path);
93
+ if (r.request_method === ep.method && regex.test(normalizedUrl)) {
94
+ const key = `${ep.method} ${ep.path}`;
95
+ const existing = endpointStatus.get(key);
96
+
97
+ // Worst status wins: api_error > test_failed > passing
98
+ if (r.response_status !== null && r.response_status >= 500) {
99
+ endpointStatus.set(key, "api_error");
100
+ } else if (r.status === "fail" || r.status === "error") {
101
+ if (existing !== "api_error") {
102
+ endpointStatus.set(key, "test_failed");
103
+ }
104
+ } else if (!existing) {
105
+ endpointStatus.set(key, "passing");
106
+ }
107
+ break;
108
+ }
109
+ }
110
+ }
111
+
112
+ let passing = 0;
113
+ let apiError = 0;
114
+ let testFailed = 0;
115
+ for (const status of endpointStatus.values()) {
116
+ if (status === "passing") passing++;
117
+ else if (status === "api_error") apiError++;
118
+ else if (status === "test_failed") testFailed++;
119
+ }
120
+
121
+ result.enriched = {
122
+ passing,
123
+ api_error: apiError,
124
+ test_failed: testFailed,
125
+ not_covered: uncovered.length,
126
+ };
127
+ }
128
+
129
+ const belowThreshold = failThreshold !== undefined && percentage < failThreshold;
130
+ return {
131
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
132
+ ...(belowThreshold ? { isError: true } : {}),
133
+ };
134
+ } catch (err) {
135
+ return {
136
+ content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
137
+ isError: true,
138
+ };
139
+ }
140
+ });
141
+ }