@kirrosh/apitool 0.4.3

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 (191) hide show
  1. package/.github/workflows/ci.yml +27 -0
  2. package/.github/workflows/release.yml +97 -0
  3. package/.mcp.json +9 -0
  4. package/APITOOL.md +195 -0
  5. package/BACKLOG.md +62 -0
  6. package/CHANGELOG.md +88 -0
  7. package/LICENSE +21 -0
  8. package/README.md +105 -0
  9. package/bun.lock +291 -0
  10. package/docs/GLOSSARY.md +182 -0
  11. package/docs/INDEX.md +21 -0
  12. package/docs/agent.md +135 -0
  13. package/docs/archive/APITOOL-pre-M22.md +831 -0
  14. package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
  15. package/docs/archive/M1-M2-parser-runner.md +216 -0
  16. package/docs/archive/M4-M7-reporter-cli.md +179 -0
  17. package/docs/archive/M5-M7-storage-junit.md +300 -0
  18. package/docs/archive/M6-webui.md +339 -0
  19. package/docs/ci.md +274 -0
  20. package/docs/generation-issues.md +67 -0
  21. package/generated/.env.yaml +3 -0
  22. package/install.ps1 +80 -0
  23. package/install.sh +113 -0
  24. package/package.json +46 -0
  25. package/scripts/run-mocked-tests.ts +45 -0
  26. package/seed-demo.ts +53 -0
  27. package/self-tests/auth.yaml +18 -0
  28. package/self-tests/collections-crud.yaml +46 -0
  29. package/self-tests/environments-crud.yaml +48 -0
  30. package/self-tests/export.yaml +32 -0
  31. package/self-tests/runs.yaml +16 -0
  32. package/src/bun-types.d.ts +5 -0
  33. package/src/cli/commands/add-api.ts +51 -0
  34. package/src/cli/commands/ai-generate.ts +106 -0
  35. package/src/cli/commands/chat.ts +43 -0
  36. package/src/cli/commands/ci-init.ts +126 -0
  37. package/src/cli/commands/collections.ts +41 -0
  38. package/src/cli/commands/coverage.ts +65 -0
  39. package/src/cli/commands/doctor.ts +127 -0
  40. package/src/cli/commands/envs.ts +218 -0
  41. package/src/cli/commands/init.ts +84 -0
  42. package/src/cli/commands/mcp.ts +16 -0
  43. package/src/cli/commands/run.ts +137 -0
  44. package/src/cli/commands/runs.ts +108 -0
  45. package/src/cli/commands/serve.ts +22 -0
  46. package/src/cli/commands/update.ts +142 -0
  47. package/src/cli/commands/validate.ts +18 -0
  48. package/src/cli/index.ts +500 -0
  49. package/src/cli/output.ts +24 -0
  50. package/src/cli/runtime.ts +7 -0
  51. package/src/core/agent/agent-loop.ts +116 -0
  52. package/src/core/agent/context-manager.ts +41 -0
  53. package/src/core/agent/system-prompt.ts +33 -0
  54. package/src/core/agent/tools/diagnose-failure.ts +51 -0
  55. package/src/core/agent/tools/explore-api.ts +40 -0
  56. package/src/core/agent/tools/index.ts +48 -0
  57. package/src/core/agent/tools/manage-environment.ts +40 -0
  58. package/src/core/agent/tools/query-results.ts +40 -0
  59. package/src/core/agent/tools/run-tests.ts +38 -0
  60. package/src/core/agent/tools/send-request.ts +44 -0
  61. package/src/core/agent/tools/validate-tests.ts +23 -0
  62. package/src/core/agent/types.ts +22 -0
  63. package/src/core/generator/ai/ai-generator.ts +61 -0
  64. package/src/core/generator/ai/llm-client.ts +159 -0
  65. package/src/core/generator/ai/output-parser.ts +307 -0
  66. package/src/core/generator/ai/prompt-builder.ts +153 -0
  67. package/src/core/generator/ai/types.ts +56 -0
  68. package/src/core/generator/coverage-scanner.ts +87 -0
  69. package/src/core/generator/data-factory.ts +115 -0
  70. package/src/core/generator/index.ts +10 -0
  71. package/src/core/generator/openapi-reader.ts +142 -0
  72. package/src/core/generator/schema-utils.ts +52 -0
  73. package/src/core/generator/serializer.ts +189 -0
  74. package/src/core/generator/types.ts +47 -0
  75. package/src/core/parser/filter.ts +14 -0
  76. package/src/core/parser/index.ts +21 -0
  77. package/src/core/parser/schema.ts +175 -0
  78. package/src/core/parser/types.ts +50 -0
  79. package/src/core/parser/variables.ts +146 -0
  80. package/src/core/parser/yaml-parser.ts +85 -0
  81. package/src/core/reporter/console.ts +175 -0
  82. package/src/core/reporter/index.ts +23 -0
  83. package/src/core/reporter/json.ts +9 -0
  84. package/src/core/reporter/junit.ts +78 -0
  85. package/src/core/reporter/types.ts +12 -0
  86. package/src/core/runner/assertions.ts +172 -0
  87. package/src/core/runner/execute-run.ts +75 -0
  88. package/src/core/runner/executor.ts +150 -0
  89. package/src/core/runner/http-client.ts +69 -0
  90. package/src/core/runner/index.ts +12 -0
  91. package/src/core/runner/types.ts +48 -0
  92. package/src/core/setup-api.ts +97 -0
  93. package/src/core/utils.ts +9 -0
  94. package/src/db/queries.ts +868 -0
  95. package/src/db/schema.ts +215 -0
  96. package/src/mcp/server.ts +47 -0
  97. package/src/mcp/tools/ci-init.ts +57 -0
  98. package/src/mcp/tools/coverage-analysis.ts +58 -0
  99. package/src/mcp/tools/explore-api.ts +84 -0
  100. package/src/mcp/tools/generate-missing-tests.ts +80 -0
  101. package/src/mcp/tools/generate-tests-guide.ts +353 -0
  102. package/src/mcp/tools/manage-environment.ts +123 -0
  103. package/src/mcp/tools/manage-server.ts +87 -0
  104. package/src/mcp/tools/query-db.ts +141 -0
  105. package/src/mcp/tools/run-tests.ts +66 -0
  106. package/src/mcp/tools/save-test-suite.ts +164 -0
  107. package/src/mcp/tools/send-request.ts +53 -0
  108. package/src/mcp/tools/setup-api.ts +49 -0
  109. package/src/mcp/tools/validate-tests.ts +42 -0
  110. package/src/tui/chat-ui.ts +150 -0
  111. package/src/web/routes/api.ts +234 -0
  112. package/src/web/routes/dashboard.ts +348 -0
  113. package/src/web/routes/runs.ts +64 -0
  114. package/src/web/schemas.ts +121 -0
  115. package/src/web/server.ts +134 -0
  116. package/src/web/static/htmx.min.js +1 -0
  117. package/src/web/static/style.css +265 -0
  118. package/src/web/views/layout.ts +46 -0
  119. package/src/web/views/results.ts +209 -0
  120. package/tests/agent/agent-loop.test.ts +61 -0
  121. package/tests/agent/context-manager.test.ts +59 -0
  122. package/tests/agent/system-prompt.test.ts +42 -0
  123. package/tests/agent/tools/diagnose-failure.test.ts +85 -0
  124. package/tests/agent/tools/explore-api.test.ts +59 -0
  125. package/tests/agent/tools/manage-environment.test.ts +78 -0
  126. package/tests/agent/tools/query-results.test.ts +77 -0
  127. package/tests/agent/tools/run-tests.test.ts +89 -0
  128. package/tests/agent/tools/send-request.test.ts +78 -0
  129. package/tests/agent/tools/validate-tests.test.ts +59 -0
  130. package/tests/ai/ai-generator.integration.test.ts +131 -0
  131. package/tests/ai/llm-client.test.ts +145 -0
  132. package/tests/ai/output-parser.test.ts +132 -0
  133. package/tests/ai/prompt-builder.test.ts +67 -0
  134. package/tests/ai/types.test.ts +55 -0
  135. package/tests/cli/args.test.ts +63 -0
  136. package/tests/cli/chat.test.ts +38 -0
  137. package/tests/cli/ci-init.test.ts +112 -0
  138. package/tests/cli/commands.test.ts +316 -0
  139. package/tests/cli/coverage.test.ts +58 -0
  140. package/tests/cli/doctor.test.ts +39 -0
  141. package/tests/cli/envs.test.ts +181 -0
  142. package/tests/cli/init.test.ts +80 -0
  143. package/tests/cli/runs.test.ts +94 -0
  144. package/tests/cli/safe-run.test.ts +103 -0
  145. package/tests/cli/update.test.ts +32 -0
  146. package/tests/core/generator/schema-utils.test.ts +108 -0
  147. package/tests/core/parser/nested-assertions.test.ts +80 -0
  148. package/tests/core/runner/root-body-assertions.test.ts +70 -0
  149. package/tests/db/chat-queries.test.ts +88 -0
  150. package/tests/db/chat-schema.test.ts +37 -0
  151. package/tests/db/environments.test.ts +131 -0
  152. package/tests/db/queries.test.ts +409 -0
  153. package/tests/db/schema.test.ts +141 -0
  154. package/tests/fixtures/.env.yaml +3 -0
  155. package/tests/fixtures/auth-token-test.yaml +8 -0
  156. package/tests/fixtures/bail/suite-a.yaml +6 -0
  157. package/tests/fixtures/bail/suite-b.yaml +6 -0
  158. package/tests/fixtures/crud.yaml +35 -0
  159. package/tests/fixtures/invalid-missing-name.yaml +5 -0
  160. package/tests/fixtures/invalid-no-method.yaml +6 -0
  161. package/tests/fixtures/petstore-auth.json +295 -0
  162. package/tests/fixtures/petstore-simple.json +151 -0
  163. package/tests/fixtures/post-only.yaml +12 -0
  164. package/tests/fixtures/simple.yaml +6 -0
  165. package/tests/fixtures/valid/.env.yaml +1 -0
  166. package/tests/fixtures/valid/a.yaml +5 -0
  167. package/tests/fixtures/valid/b.yml +5 -0
  168. package/tests/generator/coverage-scanner.test.ts +129 -0
  169. package/tests/generator/data-factory.test.ts +133 -0
  170. package/tests/generator/openapi-reader.test.ts +131 -0
  171. package/tests/integration/auth-flow.test.ts +217 -0
  172. package/tests/mcp/coverage-analysis.test.ts +64 -0
  173. package/tests/mcp/explore-api-schemas.test.ts +105 -0
  174. package/tests/mcp/explore-api.test.ts +49 -0
  175. package/tests/mcp/generate-missing-tests.test.ts +69 -0
  176. package/tests/mcp/manage-environment.test.ts +89 -0
  177. package/tests/mcp/save-test-suite.test.ts +116 -0
  178. package/tests/mcp/send-request.test.ts +79 -0
  179. package/tests/mcp/setup-api.test.ts +106 -0
  180. package/tests/mcp/tools.test.ts +248 -0
  181. package/tests/parser/schema.test.ts +134 -0
  182. package/tests/parser/variables.test.ts +227 -0
  183. package/tests/parser/yaml-parser.test.ts +69 -0
  184. package/tests/reporter/console.test.ts +256 -0
  185. package/tests/reporter/json.test.ts +98 -0
  186. package/tests/reporter/junit.test.ts +284 -0
  187. package/tests/runner/assertions.test.ts +262 -0
  188. package/tests/runner/executor.test.ts +310 -0
  189. package/tests/runner/http-client.test.ts +138 -0
  190. package/tests/web/routes.test.ts +160 -0
  191. package/tsconfig.json +31 -0
@@ -0,0 +1,353 @@
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
+
7
+ export function registerGenerateTestsGuideTool(server: McpServer) {
8
+ server.registerTool("generate_tests_guide", {
9
+ description: "Get a comprehensive guide for generating API test suites. " +
10
+ "Returns the full API specification (with request/response schemas) and a step-by-step algorithm " +
11
+ "for creating YAML test files. Use this BEFORE generating tests — it gives you " +
12
+ "everything you need to write high-quality test suites. " +
13
+ "After generating, use save_test_suite to save, run_tests to execute, and query_db(action: 'diagnose_failure') to debug.",
14
+ inputSchema: {
15
+ specPath: z.string().describe("Path or URL to OpenAPI spec file"),
16
+ outputDir: z.optional(z.string()).describe("Directory for saving test files (default: ./tests/)"),
17
+ },
18
+ }, async ({ specPath, outputDir }) => {
19
+ try {
20
+ const doc = await readOpenApiSpec(specPath);
21
+ const endpoints = extractEndpoints(doc);
22
+ const securitySchemes = extractSecuritySchemes(doc);
23
+ const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
24
+ const title = (doc as any).info?.title as string | undefined;
25
+
26
+ const apiContext = compressEndpointsWithSchemas(endpoints, securitySchemes);
27
+ const guide = buildGenerationGuide({
28
+ title: title ?? "API",
29
+ baseUrl,
30
+ apiContext,
31
+ outputDir: outputDir ?? "./tests/",
32
+ securitySchemes,
33
+ endpointCount: endpoints.length,
34
+ });
35
+
36
+ return {
37
+ content: [{ type: "text" as const, text: guide }],
38
+ };
39
+ } catch (err) {
40
+ return {
41
+ content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
42
+ isError: true,
43
+ };
44
+ }
45
+ });
46
+ }
47
+
48
+ export function compressEndpointsWithSchemas(
49
+ endpoints: EndpointInfo[],
50
+ securitySchemes: SecuritySchemeInfo[],
51
+ ): string {
52
+ const lines: string[] = [];
53
+
54
+ if (securitySchemes.length > 0) {
55
+ lines.push("SECURITY SCHEMES:");
56
+ for (const s of securitySchemes) {
57
+ let desc = ` ${s.name}: ${s.type}`;
58
+ if (s.scheme) desc += ` (${s.scheme})`;
59
+ if (s.bearerFormat) desc += ` [${s.bearerFormat}]`;
60
+ if (s.in && s.apiKeyName) desc += ` (${s.apiKeyName} in ${s.in})`;
61
+ lines.push(desc);
62
+ }
63
+ lines.push("");
64
+ }
65
+
66
+ lines.push("ENDPOINTS:");
67
+ for (const ep of endpoints) {
68
+ const summary = ep.summary ? ` — ${ep.summary}` : "";
69
+ const security = ep.security.length > 0 ? ` [auth: ${ep.security.join(", ")}]` : "";
70
+ lines.push(`\n${ep.method} ${ep.path}${summary}${security}`);
71
+
72
+ // Parameters
73
+ const pathParams = ep.parameters.filter(p => p.in === "path");
74
+ const queryParams = ep.parameters.filter(p => p.in === "query");
75
+ const headerParams = ep.parameters.filter(p => p.in === "header");
76
+ if (pathParams.length > 0) {
77
+ lines.push(` Path params: ${pathParams.map(p => formatParam(p)).join(", ")}`);
78
+ }
79
+ if (queryParams.length > 0) {
80
+ lines.push(` Query params: ${queryParams.map(p => formatParam(p)).join(", ")}`);
81
+ }
82
+ if (headerParams.length > 0) {
83
+ lines.push(` Header params: ${headerParams.map(p => formatParam(p)).join(", ")}`);
84
+ }
85
+
86
+ // Request body with full schema
87
+ if (ep.requestBodySchema) {
88
+ const contentType = ep.requestBodyContentType ?? "application/json";
89
+ const anyBody = isAnySchema(ep.requestBodySchema);
90
+ const bodyLine = anyBody
91
+ ? `any # ⚠️ spec defines body as 'any' — actual required fields unknown, test may need manual adjustment`
92
+ : compressSchema(ep.requestBodySchema);
93
+ lines.push(` Request body (${contentType}): ${bodyLine}`);
94
+ }
95
+
96
+ // Responses with schemas
97
+ for (const resp of ep.responses) {
98
+ const schemaStr = resp.schema ? ` → ${compressSchema(resp.schema)}` : "";
99
+ lines.push(` ${resp.statusCode}: ${resp.description}${schemaStr}`);
100
+ }
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }
105
+
106
+ export interface GuideOptions {
107
+ title: string;
108
+ baseUrl?: string;
109
+ apiContext: string;
110
+ outputDir: string;
111
+ securitySchemes: SecuritySchemeInfo[];
112
+ endpointCount: number;
113
+ coverageHeader?: string;
114
+ }
115
+
116
+ export function buildGenerationGuide(opts: GuideOptions): string {
117
+ const hasAuth = opts.securitySchemes.length > 0;
118
+
119
+ return `# Test Generation Guide for ${opts.title}
120
+ ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
121
+ ## API Specification (${opts.endpointCount} endpoints)
122
+ ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
123
+
124
+ ${opts.apiContext}
125
+
126
+ ${hasAuth ? `---
127
+
128
+ ## Environment Setup (Required for Authentication)
129
+
130
+ This API uses authentication. Before running tests, set up your credentials:
131
+
132
+ ### Option A — Edit the env file directly
133
+ After \`setup_api\`, the collection directory contains \`.env.default.yaml\`. Edit it to add your credentials:
134
+ \`\`\`yaml
135
+ base_url: "https://api.example.com"
136
+ api_key: "your-actual-api-key-here"
137
+ auth_token: "your-token-here"
138
+ \`\`\`
139
+
140
+ ### Option B — Use \`manage_environment\`
141
+ \`\`\`
142
+ manage_environment(action: "set", name: "default", collectionName: "your-api", variables: {"api_key": "your-key"})
143
+ \`\`\`
144
+
145
+ ### How it works
146
+ - Tests **automatically** load the \`"default"\` environment — no need to pass \`envName\` to \`run_tests\`
147
+ - If the env file is in the collection root and tests are in a \`tests/\` subdirectory, the file is still found automatically
148
+ - Use \`{{api_key}}\`, \`{{auth_token}}\`, \`{{base_url}}\` etc. in test headers/bodies
149
+ - **Never hardcode credentials** in YAML files — always use \`{{variable}}\` references
150
+
151
+ ` : ""}---
152
+
153
+ ## YAML Test Suite Format Reference
154
+
155
+ \`\`\`yaml
156
+ name: "Suite Name"
157
+ description: "What this suite tests" # optional
158
+ tags: [smoke, crud] # optional — used for filtering with --tag
159
+ base_url: "{{base_url}}"
160
+ headers: # optional suite-level headers
161
+ Authorization: "Bearer {{auth_token}}"
162
+ Content-Type: "application/json"
163
+ config: # optional
164
+ timeout: 30000
165
+ retries: 0
166
+ follow_redirects: true
167
+ tests:
168
+ - name: "Test step name"
169
+ POST: "/path/{{variable}}" # exactly ONE method key: GET, POST, PUT, PATCH, DELETE
170
+ json: # request body (object)
171
+ field: "value"
172
+ query: # query parameters
173
+ limit: "10"
174
+ headers: # step-level headers (override suite)
175
+ X-Custom: "value"
176
+ expect:
177
+ status: 200 # expected HTTP status (integer)
178
+ body: # field-level assertions
179
+ id: { type: "integer", capture: "item_id" }
180
+ name: { equals: "expected" }
181
+ email: { contains: "@", type: "string" }
182
+ count: { gt: 0, lt: 100 }
183
+ items: { exists: true } # exists must be boolean, NEVER string
184
+ pattern: { matches: "^[A-Z]+" }
185
+ headers:
186
+ Content-Type: "application/json"
187
+ duration: 5000 # max response time in ms
188
+ \`\`\`
189
+
190
+ ### Assertion Rules
191
+ - \`capture: "var_name"\` — SAVES the value into a variable (use in later steps as {{var_name}})
192
+ - \`equals: value\` — exact match COMPARISON (NEVER use equals to save a value!)
193
+ - \`type: "string"|"number"|"integer"|"boolean"|"array"|"object"\`
194
+ - \`contains: "substring"\` — string substring match
195
+ - \`matches: "regex"\` — regex pattern match
196
+ - \`gt: N\` / \`lt: N\` — numeric comparison
197
+ - \`exists: true|false\` — field presence check (MUST be boolean, not string)
198
+
199
+ ### Nested Body Assertions
200
+ Both forms are equivalent and supported:
201
+
202
+ **Dot-notation (flat):**
203
+ \`\`\`yaml
204
+ body:
205
+ "category.name": { equals: "Dogs" }
206
+ "address.city": { type: "string" }
207
+ \`\`\`
208
+
209
+ **Nested YAML (auto-flattened):**
210
+ \`\`\`yaml
211
+ body:
212
+ category:
213
+ name: { equals: "Dogs" }
214
+ address:
215
+ city: { type: "string" }
216
+ \`\`\`
217
+
218
+ ### Root Body Assertions (\`_body\`)
219
+ Use \`_body\` to assert on the response body itself (not a field inside it):
220
+
221
+ \`\`\`yaml
222
+ body:
223
+ _body: { type: "array" } # check that response body IS an array
224
+ _body: { type: "object" } # check that response body IS an object
225
+ _body: { exists: true } # check that body is not null/undefined
226
+ \`\`\`
227
+
228
+ ### Built-in Generators
229
+ Use in string values: \`{{$randomInt}}\`, \`{{$uuid}}\`, \`{{$timestamp}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`, \`{{$randomName}}\`
230
+ These are the ONLY generators — do NOT invent others.
231
+
232
+ ### Variable Interpolation
233
+ - \`{{variable}}\` in paths, bodies, headers, query params
234
+ - Captured values from previous steps are available in subsequent steps
235
+ - Environment variables from .env.yaml files: \`{{base_url}}\`, \`{{auth_username}}\`, etc.
236
+
237
+ ---
238
+
239
+ ## Step-by-Step Generation Algorithm
240
+
241
+ ### Step 0: Register the API (REQUIRED FIRST)
242
+ **Always call \`setup_api\` before generating any tests.** This registers the collection in the database so WebUI, coverage tracking, and env loading all work.
243
+ \`\`\`
244
+ setup_api(name: "myapi", specPath: "/path/to/openapi.json", dir: "/path/to/project/apis/myapi")
245
+ \`\`\`
246
+ If you skip this step, WebUI will show "No API collections registered yet" and env variables won't auto-load.
247
+
248
+ ${hasAuth ? `**Then set credentials immediately after setup_api** — use \`manage_environment\` to store the API key before touching any YAML files:
249
+ \`\`\`
250
+ manage_environment(action: "set", name: "default", collectionName: "myapi", variables: {"api_key": "<actual-key>", "base_url": "https://..."})
251
+ \`\`\`
252
+ Never put actual key values in YAML files.
253
+
254
+ ` : ""}\
255
+ ### Step 1: Analyze the API
256
+ - Identify authentication method (${hasAuth ? opts.securitySchemes.map(s => `${s.name}: ${s.type}${s.scheme ? `/${s.scheme}` : ""}`).join(", ") : "none detected"})
257
+ - Group endpoints by resource (e.g., /users/*, /pets/*, /orders/*)
258
+ - Identify CRUD patterns: POST (create) → GET (read) → PUT/PATCH (update) → DELETE
259
+ - Note required fields in request bodies
260
+
261
+ ### Step 2: Plan Test Suites
262
+ Before generating, check coverage with \`coverage_analysis\` to avoid duplicating existing tests. Use \`generate_missing_tests\` for incremental generation.
263
+
264
+ > **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.
265
+
266
+ Create separate files for each concern:
267
+ ${hasAuth ? `- \`${opts.outputDir}auth.yaml\` — Authentication flow\n` : ""}\
268
+ - \`${opts.outputDir}{resource}-crud.yaml\` — CRUD lifecycle per resource
269
+ - \`${opts.outputDir}{resource}-validation.yaml\` — Error cases per resource
270
+
271
+ ### Step 3: Generate Each Suite
272
+
273
+ ${hasAuth ? `**Auth suite** (\`auth.yaml\`):
274
+ 1. Login with valid credentials → capture token
275
+ 2. Access protected endpoint with token → 200
276
+ 3. Login with invalid credentials → 401/403
277
+ 4. Access protected endpoint without token → 401
278
+
279
+ ` : ""}\
280
+ **CRUD lifecycle** (\`{resource}-crud.yaml\`):
281
+ 1. Create resource (POST) → 201, **always verify key fields in response body** (at minimum: id, name/title)
282
+ 2. Read created resource (GET /resource/{{id}}) → 200, verify fields match what was sent
283
+ 3. List resources (GET /resource) → 200, verify \`_body: { type: "array" }\` AND \`_body.length: { gt: 0 }\`
284
+ 4. Update resource (PUT/PATCH /resource/{{id}}) → 200
285
+ 5. Read updated resource → verify changes applied
286
+ 6. Delete resource (DELETE /resource/{{id}}) → 200/204
287
+ 7. Verify deleted (GET /resource/{{id}}) → 404
288
+ 8. For bulk create endpoints (createWithArray/List): create → then GET each to verify they exist
289
+
290
+ **Validation suite** (\`{resource}-validation.yaml\`):
291
+ 1. Create with missing required fields → 400/422, verify \`message: { exists: true }\` in error body
292
+ 2. Create with invalid field types → 400/422
293
+ 3. Get non-existent resource (e.g. id=999999) → 404
294
+ 4. Delete non-existent resource → 404
295
+ 5. For error responses: always assert error body has meaningful content, not just status code
296
+
297
+ ### Step 4: Save, Run, Debug
298
+ 1. Use \`save_test_suite\` to save each file — it validates YAML before writing
299
+ 2. Use \`run_tests\` to execute — review pass/fail summary
300
+ 3. If failures: use \`query_db\` with \`action: "diagnose_failure"\` and the runId to see full request/response details
301
+ 4. Fix issues and re-save with \`overwrite: true\`
302
+
303
+ ---
304
+
305
+ ## Practical Tips
306
+
307
+ - **int64 IDs**: For APIs returning large auto-generated IDs (int64), prefer setting fixed IDs in request bodies rather than capturing auto-generated ones, as JSON number precision may cause mismatches.
308
+ - **Nested assertions**: Use dot-notation or nested YAML — both work identically.
309
+ - **Root body type**: Use \`_body: { type: "array" }\` to verify the response body type itself.
310
+ - **List endpoints**: Always check both type AND non-emptiness: \`_body: { type: "array" }\` + \`_body.length: { gt: 0 }\`
311
+ - **Create responses**: Always verify at least the key identifying fields (id, name) in the response body — don't just check status.
312
+ - **Error responses**: Assert that error bodies contain useful info (\`message: { exists: true }\`), not just status codes.
313
+ - **Bulk operations**: After bulk create (createWithArray, createWithList), add GET steps to verify resources were actually created.
314
+ - **204 No Content**: When an endpoint returns 204, omit \`body:\` assertions entirely — an empty response IS the correct behavior. Adding body assertions on 204 will always fail.
315
+
316
+ ---
317
+
318
+ ## Common Mistakes to Avoid
319
+
320
+ 1. **equals vs capture**: \`capture\` SAVES a value, \`equals\` COMPARES. To extract a token: \`{ capture: "token" }\` NOT \`{ equals: "{{token}}" }\`
321
+ 2. **exists must be boolean**: \`exists: true\` NOT \`exists: "true"\`
322
+ 3. **Status must be integer**: \`status: 200\` NOT \`status: "200"\`
323
+ 4. **One method per step**: Each test step has exactly ONE of GET/POST/PUT/PATCH/DELETE
324
+ 5. **Don't hardcode base URL**: Use \`{{base_url}}\` — set it in environment or suite base_url
325
+ 6. **Auth credentials**: Use environment variables \`{{auth_username}}\`, \`{{auth_password}}\` — NOT generators
326
+ 7. **String query params**: Query parameter values must be strings: \`limit: "10"\` not \`limit: 10\`
327
+ 8. **Hardcoded credentials**: NEVER put actual API keys/tokens in YAML — use \`{{api_key}}\` from env instead
328
+ 9. **Body assertions on 204**: Don't add \`body:\` checks for DELETE or other endpoints that return 204 No Content — the body is empty by design.
329
+
330
+ ---
331
+
332
+ ## Tools to Use
333
+
334
+ | Tool | When |
335
+ |------|------|
336
+ | \`setup_api\` | Register a new API (creates dirs, reads spec, sets up env) |
337
+ | \`generate_tests_guide\` | Get this guide for full API spec |
338
+ | \`generate_missing_tests\` | Get guide for only uncovered endpoints |
339
+ | \`save_test_suite\` | Save generated YAML (validates before writing) |
340
+ | \`run_tests\` | Execute saved test suites |
341
+ | \`query_db\` | Query runs, collections, results, diagnose failures |
342
+ | \`coverage_analysis\` | Find untested endpoints for incremental generation |
343
+ | \`explore_api\` | Re-check specific endpoints (use includeSchemas=true) |
344
+ | \`ci_init\` | Generate CI/CD workflow (GitHub Actions / GitLab CI) to run tests on push |
345
+
346
+ ## Workflow After Tests Pass
347
+
348
+ After tests are saved and running successfully, ask the user if they want to set up CI/CD:
349
+ 1. Use \`ci_init\` to generate a CI workflow (auto-detects platform or use platform param)
350
+ 2. Help them commit and push to their repository
351
+ 3. Tests will run automatically on push, PR, and on schedule
352
+ `;
353
+ }
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { getDb } from "../../db/schema.ts";
4
+ import { listEnvironmentRecords, getEnvironment, upsertEnvironment, deleteEnvironment, findCollectionByNameOrId } from "../../db/queries.ts";
5
+
6
+ export function registerManageEnvironmentTool(server: McpServer, dbPath?: string) {
7
+ server.registerTool("manage_environment", {
8
+ description: "Manage environments — list, get, set, or delete environment variables used for API test execution. Use collectionName to scope environments to a specific API.",
9
+ inputSchema: {
10
+ action: z.enum(["list", "get", "set", "delete"]).describe("Action: list, get, set, or delete"),
11
+ name: z.optional(z.string()).describe("Environment name (required for get/set/delete)"),
12
+ variables: z.optional(z.string()).describe("Variables as JSON string (for set action, e.g. '{\"base_url\": \"...\"}')"),
13
+ collectionName: z.optional(z.string()).describe("API/collection name or ID to scope the environment to"),
14
+ },
15
+ }, async ({ action, name, variables, collectionName }) => {
16
+ try {
17
+ getDb(dbPath);
18
+
19
+ // Resolve collection if provided
20
+ let collectionId: number | undefined;
21
+ if (collectionName) {
22
+ const col = findCollectionByNameOrId(collectionName);
23
+ if (!col) {
24
+ return {
25
+ content: [{ type: "text" as const, text: JSON.stringify({ error: `API '${collectionName}' not found` }, null, 2) }],
26
+ isError: true,
27
+ };
28
+ }
29
+ collectionId = col.id;
30
+ }
31
+
32
+ switch (action) {
33
+ case "list": {
34
+ const envs = listEnvironmentRecords(collectionId);
35
+ const safe = envs.map(e => ({
36
+ id: e.id,
37
+ name: e.name,
38
+ collection_id: e.collection_id,
39
+ variables: Object.keys(e.variables),
40
+ }));
41
+ return {
42
+ content: [{ type: "text" as const, text: JSON.stringify(safe, null, 2) }],
43
+ };
44
+ }
45
+
46
+ case "get": {
47
+ if (!name) {
48
+ return {
49
+ content: [{ type: "text" as const, text: JSON.stringify({ error: "name is required for get action" }, null, 2) }],
50
+ isError: true,
51
+ };
52
+ }
53
+ const vars = getEnvironment(name, collectionId);
54
+ if (!vars) {
55
+ return {
56
+ content: [{ type: "text" as const, text: JSON.stringify({ error: `Environment '${name}' not found` }, null, 2) }],
57
+ isError: true,
58
+ };
59
+ }
60
+ return {
61
+ content: [{ type: "text" as const, text: JSON.stringify({ name, variables: vars }, null, 2) }],
62
+ };
63
+ }
64
+
65
+ case "set": {
66
+ if (!name || !variables) {
67
+ return {
68
+ content: [{ type: "text" as const, text: JSON.stringify({ error: "name and variables are required for set action" }, null, 2) }],
69
+ isError: true,
70
+ };
71
+ }
72
+ let parsedVars: Record<string, string>;
73
+ try {
74
+ parsedVars = JSON.parse(variables);
75
+ } catch {
76
+ return {
77
+ content: [{ type: "text" as const, text: JSON.stringify({ error: "variables must be a valid JSON string" }, null, 2) }],
78
+ isError: true,
79
+ };
80
+ }
81
+ upsertEnvironment(name, parsedVars, collectionId);
82
+ return {
83
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, name, collection_id: collectionId ?? null }, null, 2) }],
84
+ };
85
+ }
86
+
87
+ case "delete": {
88
+ if (!name) {
89
+ return {
90
+ content: [{ type: "text" as const, text: JSON.stringify({ error: "name is required for delete action" }, null, 2) }],
91
+ isError: true,
92
+ };
93
+ }
94
+ const envs = listEnvironmentRecords(collectionId);
95
+ const env = collectionId
96
+ ? envs.find(e => e.name === name && e.collection_id === collectionId)
97
+ : envs.find(e => e.name === name && e.collection_id === null);
98
+ if (!env) {
99
+ return {
100
+ content: [{ type: "text" as const, text: JSON.stringify({ error: `Environment '${name}' not found` }, null, 2) }],
101
+ isError: true,
102
+ };
103
+ }
104
+ deleteEnvironment(env.id);
105
+ return {
106
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, deleted: name }, null, 2) }],
107
+ };
108
+ }
109
+
110
+ default:
111
+ return {
112
+ content: [{ type: "text" as const, text: JSON.stringify({ error: `Unknown action: ${action}` }, null, 2) }],
113
+ isError: true,
114
+ };
115
+ }
116
+ } catch (err) {
117
+ return {
118
+ content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
119
+ isError: true,
120
+ };
121
+ }
122
+ });
123
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
4
+ let serverPort: number = 0;
5
+
6
+ export function registerManageServerTool(server: McpServer, dbPath?: string) {
7
+ server.registerTool("manage_server", {
8
+ description:
9
+ "Start, stop, restart, or check status of the apitool WebUI server. " +
10
+ "Useful for viewing test results in a browser without leaving the MCP session.",
11
+ inputSchema: {
12
+ action: z.enum(["start", "stop", "restart", "status"]).describe("Action to perform"),
13
+ port: z.optional(z.number().int().min(1).max(65535)).describe("Port number (default: 8080, only for start/restart)"),
14
+ },
15
+ }, async ({ action, port }) => {
16
+ const targetPort = port ?? 8080;
17
+
18
+ switch (action) {
19
+ case "start": {
20
+ if (serverInstance) {
21
+ return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}`, message: "Server already running" });
22
+ }
23
+ return await startServer(targetPort, dbPath);
24
+ }
25
+
26
+ case "stop": {
27
+ if (!serverInstance) {
28
+ return result({ running: false, message: "Server is not running" });
29
+ }
30
+ serverInstance.stop();
31
+ serverInstance = null;
32
+ const stoppedPort = serverPort;
33
+ serverPort = 0;
34
+ return result({ running: false, message: `Server stopped (was on port ${stoppedPort})` });
35
+ }
36
+
37
+ case "restart": {
38
+ if (serverInstance) {
39
+ serverInstance.stop();
40
+ serverInstance = null;
41
+ serverPort = 0;
42
+ }
43
+ return await startServer(targetPort, dbPath);
44
+ }
45
+
46
+ case "status": {
47
+ if (serverInstance) {
48
+ return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}` });
49
+ }
50
+ return result({ running: false });
51
+ }
52
+ }
53
+ });
54
+ }
55
+
56
+ async function startServer(port: number, dbPath?: string) {
57
+ try {
58
+ const { getDb } = await import("../../db/schema.ts");
59
+ const { createApp } = await import("../../web/server.ts");
60
+
61
+ getDb(dbPath);
62
+ const app = createApp();
63
+
64
+ serverInstance = Bun.serve({
65
+ fetch: app.fetch,
66
+ port,
67
+ hostname: "0.0.0.0",
68
+ });
69
+ serverPort = port;
70
+
71
+ return result({ running: true, port, url: `http://localhost:${port}`, message: "Server started" });
72
+ } catch (err) {
73
+ return {
74
+ content: [{ type: "text" as const, text: JSON.stringify({
75
+ running: false,
76
+ error: (err as Error).message,
77
+ }, null, 2) }],
78
+ isError: true,
79
+ };
80
+ }
81
+ }
82
+
83
+ function result(data: Record<string, unknown>) {
84
+ return {
85
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
86
+ };
87
+ }