@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,391 @@
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
+ }
54
+
55
+ export function compressEndpointsWithSchemas(
56
+ endpoints: EndpointInfo[],
57
+ securitySchemes: SecuritySchemeInfo[],
58
+ ): string {
59
+ const lines: string[] = [];
60
+
61
+ if (securitySchemes.length > 0) {
62
+ lines.push("SECURITY SCHEMES:");
63
+ for (const s of securitySchemes) {
64
+ let desc = ` ${s.name}: ${s.type}`;
65
+ if (s.scheme) desc += ` (${s.scheme})`;
66
+ if (s.bearerFormat) desc += ` [${s.bearerFormat}]`;
67
+ if (s.in && s.apiKeyName) desc += ` (${s.apiKeyName} in ${s.in})`;
68
+ lines.push(desc);
69
+ }
70
+ lines.push("");
71
+ }
72
+
73
+ lines.push("ENDPOINTS:");
74
+ for (const ep of endpoints) {
75
+ const summary = ep.summary ? ` — ${ep.summary}` : "";
76
+ const security = ep.security.length > 0 ? ` [auth: ${ep.security.join(", ")}]` : "";
77
+ lines.push(`\n${ep.method} ${ep.path}${summary}${security}`);
78
+
79
+ // Parameters
80
+ const pathParams = ep.parameters.filter(p => p.in === "path");
81
+ const queryParams = ep.parameters.filter(p => p.in === "query");
82
+ const headerParams = ep.parameters.filter(p => p.in === "header");
83
+ if (pathParams.length > 0) {
84
+ lines.push(` Path params: ${pathParams.map(p => formatParam(p)).join(", ")}`);
85
+ }
86
+ if (queryParams.length > 0) {
87
+ lines.push(` Query params: ${queryParams.map(p => formatParam(p)).join(", ")}`);
88
+ }
89
+ if (headerParams.length > 0) {
90
+ lines.push(` Header params: ${headerParams.map(p => formatParam(p)).join(", ")}`);
91
+ }
92
+
93
+ // Request body with full schema
94
+ if (ep.requestBodySchema) {
95
+ const contentType = ep.requestBodyContentType ?? "application/json";
96
+ const anyBody = isAnySchema(ep.requestBodySchema);
97
+ const bodyLine = anyBody
98
+ ? `any # ⚠️ spec defines body as 'any' — actual required fields unknown, test may need manual adjustment`
99
+ : compressSchema(ep.requestBodySchema);
100
+ lines.push(` Request body (${contentType}): ${bodyLine}`);
101
+ }
102
+
103
+ // Responses with schemas
104
+ for (const resp of ep.responses) {
105
+ const schemaStr = resp.schema ? ` → ${compressSchema(resp.schema)}` : "";
106
+ lines.push(` ${resp.statusCode}: ${resp.description}${schemaStr}`);
107
+ }
108
+ }
109
+
110
+ return lines.join("\n");
111
+ }
112
+
113
+ export interface GuideOptions {
114
+ title: string;
115
+ baseUrl?: string;
116
+ apiContext: string;
117
+ outputDir: string;
118
+ securitySchemes: SecuritySchemeInfo[];
119
+ endpointCount: number;
120
+ coverageHeader?: string;
121
+ }
122
+
123
+ export function buildGenerationGuide(opts: GuideOptions): string {
124
+ const hasAuth = opts.securitySchemes.length > 0;
125
+
126
+ return `# Test Generation Guide for ${opts.title}
127
+ ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
128
+ ## API Specification (${opts.endpointCount} endpoints)
129
+ ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
130
+
131
+ ${opts.apiContext}
132
+
133
+ ${hasAuth ? `---
134
+
135
+ ## Environment Setup (Required for Authentication)
136
+
137
+ This API uses authentication. Before running tests, set up your credentials:
138
+
139
+ ### Edit the env file
140
+ After \`setup_api\`, the collection directory contains \`.env.yaml\`. Edit it to add your credentials:
141
+ \`\`\`yaml
142
+ base_url: "https://api.example.com"
143
+ api_key: "your-actual-api-key-here"
144
+ auth_token: "your-token-here"
145
+ \`\`\`
146
+
147
+ ### How it works
148
+ - Tests **automatically** load the \`"default"\` environment — no need to pass \`envName\` to \`run_tests\`
149
+ - If the env file is in the collection root and tests are in a \`tests/\` subdirectory, the file is still found automatically
150
+ - Use \`{{api_key}}\`, \`{{auth_token}}\`, \`{{base_url}}\` etc. in test headers/bodies
151
+ - **Never hardcode credentials** in YAML files — always use \`{{variable}}\` references
152
+
153
+ ` : ""}---
154
+
155
+ ## YAML Test Suite Format Reference
156
+
157
+ \`\`\`yaml
158
+ name: "Suite Name"
159
+ description: "What this suite tests" # optional
160
+ tags: [smoke, crud] # optional — used for filtering with --tag
161
+ base_url: "{{base_url}}"
162
+ headers: # optional suite-level headers
163
+ Authorization: "Bearer {{auth_token}}"
164
+ Content-Type: "application/json"
165
+ config: # optional
166
+ timeout: 30000
167
+ retries: 0
168
+ follow_redirects: true
169
+ tests:
170
+ - name: "Test step name"
171
+ POST: "/path/{{variable}}" # exactly ONE method key: GET, POST, PUT, PATCH, DELETE
172
+ json: # request body (object)
173
+ field: "value"
174
+ query: # query parameters
175
+ limit: "10"
176
+ headers: # step-level headers (override suite)
177
+ X-Custom: "value"
178
+ expect:
179
+ status: 200 # expected HTTP status: integer OR array [200, 204]
180
+ body: # field-level assertions
181
+ id: { type: "integer", capture: "item_id" }
182
+ name: { equals: "expected" }
183
+ email: { contains: "@", type: "string" }
184
+ count: { gt: 0, lt: 100 }
185
+ items: { exists: true } # exists must be boolean, NEVER string
186
+ pattern: { matches: "^[A-Z]+" }
187
+ headers:
188
+ Content-Type: "application/json"
189
+ duration: 5000 # max response time in ms
190
+ \`\`\`
191
+
192
+ ### Assertion Rules
193
+ - \`capture: "var_name"\` — SAVES the value into a variable (use in later steps as {{var_name}})
194
+ - \`equals: value\` — exact match COMPARISON (NEVER use equals to save a value!)
195
+ - \`type: "string"|"number"|"integer"|"boolean"|"array"|"object"\`
196
+ - \`contains: "substring"\` — string substring match
197
+ - \`matches: "regex"\` — regex pattern match
198
+ - \`gt: N\` / \`lt: N\` — numeric comparison
199
+ - \`exists: true|false\` — field presence check (MUST be boolean, not string)
200
+
201
+ ### Nested Body Assertions
202
+ Both forms are equivalent and supported:
203
+
204
+ **Dot-notation (flat):**
205
+ \`\`\`yaml
206
+ body:
207
+ "category.name": { equals: "Dogs" }
208
+ "address.city": { type: "string" }
209
+ \`\`\`
210
+
211
+ **Nested YAML (auto-flattened):**
212
+ \`\`\`yaml
213
+ body:
214
+ category:
215
+ name: { equals: "Dogs" }
216
+ address:
217
+ city: { type: "string" }
218
+ \`\`\`
219
+
220
+ ### Root Body Assertions (\`_body\`)
221
+ Use \`_body\` to assert on the response body itself (not a field inside it):
222
+
223
+ \`\`\`yaml
224
+ body:
225
+ _body: { type: "array" } # check that response body IS an array
226
+ _body: { type: "object" } # check that response body IS an object
227
+ _body: { exists: true } # check that body is not null/undefined
228
+ \`\`\`
229
+
230
+ ### Built-in Generators
231
+ Use in string values: \`{{$randomInt}}\`, \`{{$uuid}}\`, \`{{$timestamp}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`, \`{{$randomName}}\`
232
+ These are the ONLY generators — do NOT invent others.
233
+
234
+ ### Variable Interpolation
235
+ - \`{{variable}}\` in paths, bodies, headers, query params
236
+ - Captured values from previous steps are available in subsequent steps
237
+ - Environment variables from .env.yaml files: \`{{base_url}}\`, \`{{auth_username}}\`, etc.
238
+
239
+ ---
240
+
241
+ ## Step-by-Step Generation Algorithm
242
+
243
+ ### Step 0: Register the API (REQUIRED FIRST)
244
+ **Always call \`setup_api\` before generating any tests.** This registers the collection in the database so WebUI, coverage tracking, and env loading all work.
245
+ \`\`\`
246
+ setup_api(name: "myapi", specPath: "/path/to/openapi.json", dir: "/path/to/project/apis/myapi")
247
+ \`\`\`
248
+ If you skip this step, WebUI will show "No API collections registered yet" and env variables won't auto-load.
249
+
250
+ ${hasAuth ? `**Then set credentials immediately after setup_api** — edit the \`.env.yaml\` file in the API directory to add your credentials:
251
+ \`\`\`yaml
252
+ api_key: "<actual-key>"
253
+ base_url: "https://..."
254
+ \`\`\`
255
+ Never put actual key values directly in YAML test files.
256
+
257
+ ` : ""}\
258
+ ### Step 1: Analyze the API
259
+ - Identify authentication method (${hasAuth ? opts.securitySchemes.map(s => `${s.name}: ${s.type}${s.scheme ? `/${s.scheme}` : ""}`).join(", ") : "none detected"})
260
+ - Group endpoints by resource (e.g., /users/*, /pets/*, /orders/*)
261
+ - Identify CRUD patterns: POST (create) → GET (read) → PUT/PATCH (update) → DELETE
262
+ - Note required fields in request bodies
263
+
264
+ ### 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.
266
+
267
+ > **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
+
269
+ Create separate files for each concern:
270
+ ${hasAuth ? `- \`${opts.outputDir}auth.yaml\` — Authentication flow\n` : ""}\
271
+ - \`${opts.outputDir}{resource}-crud.yaml\` — CRUD lifecycle per resource
272
+ - \`${opts.outputDir}{resource}-validation.yaml\` — Error cases per resource
273
+
274
+ ### Step 3: Generate Each Suite
275
+
276
+ ${hasAuth ? `**Auth suite** (\`auth.yaml\`):
277
+ 1. Login with valid credentials → capture token
278
+ 2. Access protected endpoint with token → 200
279
+ 3. Login with invalid credentials → 401/403
280
+ 4. Access protected endpoint without token → 401
281
+
282
+ ` : ""}\
283
+ **CRUD lifecycle** (\`{resource}-crud.yaml\`):
284
+ 1. Create resource (POST) → 201, **always verify key fields in response body** (at minimum: id, name/title)
285
+ 2. Read created resource (GET /resource/{{id}}) → 200, verify fields match what was sent
286
+ 3. List resources (GET /resource) → 200, verify \`_body: { type: "array" }\` AND \`_body.length: { gt: 0 }\`
287
+ 4. Update resource (PUT/PATCH /resource/{{id}}) → 200
288
+ 5. Read updated resource → verify changes applied
289
+ 6. Delete resource (DELETE /resource/{{id}}) → 200/204
290
+ 7. Verify deleted (GET /resource/{{id}}) → 404
291
+ 8. For bulk create endpoints (createWithArray/List): create → then GET each to verify they exist
292
+
293
+ **Validation suite** (\`{resource}-validation.yaml\`):
294
+ 1. Create with missing required fields → 400/422, verify \`message: { exists: true }\` in error body
295
+ 2. Create with invalid field types → 400/422
296
+ 3. Get non-existent resource (e.g. id=999999) → 404
297
+ 4. Delete non-existent resource → 404
298
+ 5. For error responses: always assert error body has meaningful content, not just status code
299
+
300
+ ### Step 4: Save, Run, Debug
301
+ 1. Use \`save_test_suite\` to save each file — it validates YAML before writing
302
+ 2. Use \`run_tests\` to execute — review pass/fail summary
303
+ 3. If failures: use \`query_db\` with \`action: "diagnose_failure"\` and the runId to see full request/response details
304
+ 4. Fix issues and re-save with \`overwrite: true\`
305
+
306
+ ---
307
+
308
+ ## Tag Conventions
309
+
310
+ Use standard tags to enable safe filtering:
311
+
312
+ | Tag | HTTP Methods | Safe for |
313
+ |-----|-------------|---------|
314
+ | \`smoke\` | GET only | Production (read-only, zero risk) |
315
+ | \`crud\` | POST/PUT/PATCH | Staging only (state-changing) |
316
+ | \`destructive\` | DELETE | Explicit opt-in, run last |
317
+ | \`auth\` | Any (auth flows) | Run first to capture tokens |
318
+
319
+ Example: \`zond run --tag smoke --safe\` → reads-only, safe against production.
320
+
321
+ ---
322
+
323
+ ## Practical Tips
324
+
325
+ - **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.
326
+ - **Nested assertions**: Use dot-notation or nested YAML — both work identically.
327
+ - **Root body type**: Use \`_body: { type: "array" }\` to verify the response body type itself.
328
+ - **List endpoints**: Always check both type AND non-emptiness: \`_body: { type: "array" }\` + \`_body.length: { gt: 0 }\`
329
+ - **Create responses**: Always verify at least the key identifying fields (id, name) in the response body — don't just check status.
330
+ - **Error responses**: Assert that error bodies contain useful info (\`message: { exists: true }\`), not just status codes.
331
+ - **Bulk operations**: After bulk create (createWithArray, createWithList), add GET steps to verify resources were actually created.
332
+ - **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.
333
+ - **Cleanup pattern**: Always delete test data in the same suite. Use a create → read → delete lifecycle so tests are idempotent:
334
+ \`\`\`yaml
335
+ tests:
336
+ - name: Create test resource
337
+ POST: /users
338
+ json: { name: "zond-test-{{$randomString}}" }
339
+ expect:
340
+ status: 201
341
+ body:
342
+ id: { capture: user_id }
343
+ - name: Read created resource
344
+ GET: /users/{{user_id}}
345
+ expect:
346
+ status: 200
347
+ - name: Cleanup - delete test resource
348
+ DELETE: /users/{{user_id}}
349
+ expect:
350
+ status: 204
351
+ \`\`\`
352
+ - **Identifiable test data**: Prefix test data with \`zond-test-\` or use \`{{$uuid}}\` / \`zond-test-{{$randomString}}\` so you can identify and clean up leftover test data if needed.
353
+
354
+ ---
355
+
356
+ ## Common Mistakes to Avoid
357
+
358
+ 1. **equals vs capture**: \`capture\` SAVES a value, \`equals\` COMPARES. To extract a token: \`{ capture: "token" }\` NOT \`{ equals: "{{token}}" }\`
359
+ 2. **exists must be boolean**: \`exists: true\` NOT \`exists: "true"\`
360
+ 3. **Status must be integer or array**: \`status: 200\` or \`status: [200, 204]\` NOT \`status: "200"\`
361
+ 4. **One method per step**: Each test step has exactly ONE of GET/POST/PUT/PATCH/DELETE
362
+ 5. **Don't hardcode base URL**: Use \`{{base_url}}\` — set it in environment or suite base_url
363
+ 6. **Auth credentials**: Use environment variables \`{{auth_username}}\`, \`{{auth_password}}\` — NOT generators
364
+ 7. **String query params**: Query parameter values must be strings: \`limit: "10"\` not \`limit: 10\`
365
+ 8. **Hardcoded credentials**: NEVER put actual API keys/tokens in YAML — use \`{{api_key}}\` from env instead
366
+ 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.
367
+
368
+ ---
369
+
370
+ ## Tools to Use
371
+
372
+ | Tool | When |
373
+ |------|------|
374
+ | \`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 |
377
+ | \`save_test_suite\` | Save generated YAML (validates before writing) |
378
+ | \`run_tests\` | Execute saved test suites |
379
+ | \`query_db\` | Query runs, collections, results, diagnose failures |
380
+ | \`coverage_analysis\` | Find untested endpoints for incremental generation |
381
+ | \`explore_api\` | Re-check specific endpoints (use includeSchemas=true) |
382
+ | \`ci_init\` | Generate CI/CD workflow (GitHub Actions / GitLab CI) to run tests on push |
383
+
384
+ ## Workflow After Tests Pass
385
+
386
+ After tests are saved and running successfully, ask the user if they want to set up CI/CD:
387
+ 1. Use \`ci_init\` to generate a CI workflow (auto-detects platform or use platform param)
388
+ 2. Help them commit and push to their repository
389
+ 3. Tests will run automatically on push, PR, and on schedule
390
+ `;
391
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { TOOL_DESCRIPTIONS } from "../descriptions.js";
4
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
5
+ let serverPort: number = 0;
6
+
7
+ export function registerManageServerTool(server: McpServer, dbPath?: string) {
8
+ server.registerTool("manage_server", {
9
+ description: TOOL_DESCRIPTIONS.manage_server,
10
+ inputSchema: {
11
+ action: z.enum(["start", "stop", "restart", "status"]).describe("Action to perform"),
12
+ port: z.optional(z.number().int().min(1).max(65535)).describe("Port number (default: 8080, only for start/restart)"),
13
+ },
14
+ }, async ({ action, port }) => {
15
+ const targetPort = port ?? 8080;
16
+
17
+ switch (action) {
18
+ case "start": {
19
+ if (serverInstance) {
20
+ return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}`, message: "Server already running" });
21
+ }
22
+ return await startServer(targetPort, dbPath);
23
+ }
24
+
25
+ case "stop": {
26
+ if (!serverInstance) {
27
+ return result({ running: false, message: "Server is not running" });
28
+ }
29
+ serverInstance.stop();
30
+ serverInstance = null;
31
+ const stoppedPort = serverPort;
32
+ serverPort = 0;
33
+ return result({ running: false, message: `Server stopped (was on port ${stoppedPort})` });
34
+ }
35
+
36
+ case "restart": {
37
+ if (serverInstance) {
38
+ serverInstance.stop();
39
+ serverInstance = null;
40
+ serverPort = 0;
41
+ }
42
+ return await startServer(targetPort, dbPath);
43
+ }
44
+
45
+ case "status": {
46
+ if (serverInstance) {
47
+ return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}` });
48
+ }
49
+ return result({ running: false });
50
+ }
51
+ }
52
+ });
53
+ }
54
+
55
+ async function startServer(port: number, dbPath?: string) {
56
+ try {
57
+ const { getDb } = await import("../../db/schema.ts");
58
+ const { createApp } = await import("../../web/server.ts");
59
+
60
+ getDb(dbPath);
61
+ const app = createApp();
62
+
63
+ serverInstance = Bun.serve({
64
+ fetch: app.fetch,
65
+ port,
66
+ hostname: "0.0.0.0",
67
+ });
68
+ serverPort = port;
69
+
70
+ return result({ running: true, port, url: `http://localhost:${port}`, message: "Server started" });
71
+ } catch (err) {
72
+ return {
73
+ content: [{ type: "text" as const, text: JSON.stringify({
74
+ running: false,
75
+ error: (err as Error).message,
76
+ }, null, 2) }],
77
+ isError: true,
78
+ };
79
+ }
80
+ }
81
+
82
+ function result(data: Record<string, unknown>) {
83
+ return {
84
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
85
+ };
86
+ }