@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.
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/release.yml +97 -0
- package/.mcp.json +9 -0
- package/APITOOL.md +195 -0
- package/BACKLOG.md +62 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bun.lock +291 -0
- package/docs/GLOSSARY.md +182 -0
- package/docs/INDEX.md +21 -0
- package/docs/agent.md +135 -0
- package/docs/archive/APITOOL-pre-M22.md +831 -0
- package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
- package/docs/archive/M1-M2-parser-runner.md +216 -0
- package/docs/archive/M4-M7-reporter-cli.md +179 -0
- package/docs/archive/M5-M7-storage-junit.md +300 -0
- package/docs/archive/M6-webui.md +339 -0
- package/docs/ci.md +274 -0
- package/docs/generation-issues.md +67 -0
- package/generated/.env.yaml +3 -0
- package/install.ps1 +80 -0
- package/install.sh +113 -0
- package/package.json +46 -0
- package/scripts/run-mocked-tests.ts +45 -0
- package/seed-demo.ts +53 -0
- package/self-tests/auth.yaml +18 -0
- package/self-tests/collections-crud.yaml +46 -0
- package/self-tests/environments-crud.yaml +48 -0
- package/self-tests/export.yaml +32 -0
- package/self-tests/runs.yaml +16 -0
- package/src/bun-types.d.ts +5 -0
- package/src/cli/commands/add-api.ts +51 -0
- package/src/cli/commands/ai-generate.ts +106 -0
- package/src/cli/commands/chat.ts +43 -0
- package/src/cli/commands/ci-init.ts +126 -0
- package/src/cli/commands/collections.ts +41 -0
- package/src/cli/commands/coverage.ts +65 -0
- package/src/cli/commands/doctor.ts +127 -0
- package/src/cli/commands/envs.ts +218 -0
- package/src/cli/commands/init.ts +84 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/run.ts +137 -0
- package/src/cli/commands/runs.ts +108 -0
- package/src/cli/commands/serve.ts +22 -0
- package/src/cli/commands/update.ts +142 -0
- package/src/cli/commands/validate.ts +18 -0
- package/src/cli/index.ts +500 -0
- package/src/cli/output.ts +24 -0
- package/src/cli/runtime.ts +7 -0
- package/src/core/agent/agent-loop.ts +116 -0
- package/src/core/agent/context-manager.ts +41 -0
- package/src/core/agent/system-prompt.ts +33 -0
- package/src/core/agent/tools/diagnose-failure.ts +51 -0
- package/src/core/agent/tools/explore-api.ts +40 -0
- package/src/core/agent/tools/index.ts +48 -0
- package/src/core/agent/tools/manage-environment.ts +40 -0
- package/src/core/agent/tools/query-results.ts +40 -0
- package/src/core/agent/tools/run-tests.ts +38 -0
- package/src/core/agent/tools/send-request.ts +44 -0
- package/src/core/agent/tools/validate-tests.ts +23 -0
- package/src/core/agent/types.ts +22 -0
- package/src/core/generator/ai/ai-generator.ts +61 -0
- package/src/core/generator/ai/llm-client.ts +159 -0
- package/src/core/generator/ai/output-parser.ts +307 -0
- package/src/core/generator/ai/prompt-builder.ts +153 -0
- package/src/core/generator/ai/types.ts +56 -0
- package/src/core/generator/coverage-scanner.ts +87 -0
- package/src/core/generator/data-factory.ts +115 -0
- package/src/core/generator/index.ts +10 -0
- package/src/core/generator/openapi-reader.ts +142 -0
- package/src/core/generator/schema-utils.ts +52 -0
- package/src/core/generator/serializer.ts +189 -0
- package/src/core/generator/types.ts +47 -0
- package/src/core/parser/filter.ts +14 -0
- package/src/core/parser/index.ts +21 -0
- package/src/core/parser/schema.ts +175 -0
- package/src/core/parser/types.ts +50 -0
- package/src/core/parser/variables.ts +146 -0
- package/src/core/parser/yaml-parser.ts +85 -0
- package/src/core/reporter/console.ts +175 -0
- package/src/core/reporter/index.ts +23 -0
- package/src/core/reporter/json.ts +9 -0
- package/src/core/reporter/junit.ts +78 -0
- package/src/core/reporter/types.ts +12 -0
- package/src/core/runner/assertions.ts +172 -0
- package/src/core/runner/execute-run.ts +75 -0
- package/src/core/runner/executor.ts +150 -0
- package/src/core/runner/http-client.ts +69 -0
- package/src/core/runner/index.ts +12 -0
- package/src/core/runner/types.ts +48 -0
- package/src/core/setup-api.ts +97 -0
- package/src/core/utils.ts +9 -0
- package/src/db/queries.ts +868 -0
- package/src/db/schema.ts +215 -0
- package/src/mcp/server.ts +47 -0
- package/src/mcp/tools/ci-init.ts +57 -0
- package/src/mcp/tools/coverage-analysis.ts +58 -0
- package/src/mcp/tools/explore-api.ts +84 -0
- package/src/mcp/tools/generate-missing-tests.ts +80 -0
- package/src/mcp/tools/generate-tests-guide.ts +353 -0
- package/src/mcp/tools/manage-environment.ts +123 -0
- package/src/mcp/tools/manage-server.ts +87 -0
- package/src/mcp/tools/query-db.ts +141 -0
- package/src/mcp/tools/run-tests.ts +66 -0
- package/src/mcp/tools/save-test-suite.ts +164 -0
- package/src/mcp/tools/send-request.ts +53 -0
- package/src/mcp/tools/setup-api.ts +49 -0
- package/src/mcp/tools/validate-tests.ts +42 -0
- package/src/tui/chat-ui.ts +150 -0
- package/src/web/routes/api.ts +234 -0
- package/src/web/routes/dashboard.ts +348 -0
- package/src/web/routes/runs.ts +64 -0
- package/src/web/schemas.ts +121 -0
- package/src/web/server.ts +134 -0
- package/src/web/static/htmx.min.js +1 -0
- package/src/web/static/style.css +265 -0
- package/src/web/views/layout.ts +46 -0
- package/src/web/views/results.ts +209 -0
- package/tests/agent/agent-loop.test.ts +61 -0
- package/tests/agent/context-manager.test.ts +59 -0
- package/tests/agent/system-prompt.test.ts +42 -0
- package/tests/agent/tools/diagnose-failure.test.ts +85 -0
- package/tests/agent/tools/explore-api.test.ts +59 -0
- package/tests/agent/tools/manage-environment.test.ts +78 -0
- package/tests/agent/tools/query-results.test.ts +77 -0
- package/tests/agent/tools/run-tests.test.ts +89 -0
- package/tests/agent/tools/send-request.test.ts +78 -0
- package/tests/agent/tools/validate-tests.test.ts +59 -0
- package/tests/ai/ai-generator.integration.test.ts +131 -0
- package/tests/ai/llm-client.test.ts +145 -0
- package/tests/ai/output-parser.test.ts +132 -0
- package/tests/ai/prompt-builder.test.ts +67 -0
- package/tests/ai/types.test.ts +55 -0
- package/tests/cli/args.test.ts +63 -0
- package/tests/cli/chat.test.ts +38 -0
- package/tests/cli/ci-init.test.ts +112 -0
- package/tests/cli/commands.test.ts +316 -0
- package/tests/cli/coverage.test.ts +58 -0
- package/tests/cli/doctor.test.ts +39 -0
- package/tests/cli/envs.test.ts +181 -0
- package/tests/cli/init.test.ts +80 -0
- package/tests/cli/runs.test.ts +94 -0
- package/tests/cli/safe-run.test.ts +103 -0
- package/tests/cli/update.test.ts +32 -0
- package/tests/core/generator/schema-utils.test.ts +108 -0
- package/tests/core/parser/nested-assertions.test.ts +80 -0
- package/tests/core/runner/root-body-assertions.test.ts +70 -0
- package/tests/db/chat-queries.test.ts +88 -0
- package/tests/db/chat-schema.test.ts +37 -0
- package/tests/db/environments.test.ts +131 -0
- package/tests/db/queries.test.ts +409 -0
- package/tests/db/schema.test.ts +141 -0
- package/tests/fixtures/.env.yaml +3 -0
- package/tests/fixtures/auth-token-test.yaml +8 -0
- package/tests/fixtures/bail/suite-a.yaml +6 -0
- package/tests/fixtures/bail/suite-b.yaml +6 -0
- package/tests/fixtures/crud.yaml +35 -0
- package/tests/fixtures/invalid-missing-name.yaml +5 -0
- package/tests/fixtures/invalid-no-method.yaml +6 -0
- package/tests/fixtures/petstore-auth.json +295 -0
- package/tests/fixtures/petstore-simple.json +151 -0
- package/tests/fixtures/post-only.yaml +12 -0
- package/tests/fixtures/simple.yaml +6 -0
- package/tests/fixtures/valid/.env.yaml +1 -0
- package/tests/fixtures/valid/a.yaml +5 -0
- package/tests/fixtures/valid/b.yml +5 -0
- package/tests/generator/coverage-scanner.test.ts +129 -0
- package/tests/generator/data-factory.test.ts +133 -0
- package/tests/generator/openapi-reader.test.ts +131 -0
- package/tests/integration/auth-flow.test.ts +217 -0
- package/tests/mcp/coverage-analysis.test.ts +64 -0
- package/tests/mcp/explore-api-schemas.test.ts +105 -0
- package/tests/mcp/explore-api.test.ts +49 -0
- package/tests/mcp/generate-missing-tests.test.ts +69 -0
- package/tests/mcp/manage-environment.test.ts +89 -0
- package/tests/mcp/save-test-suite.test.ts +116 -0
- package/tests/mcp/send-request.test.ts +79 -0
- package/tests/mcp/setup-api.test.ts +106 -0
- package/tests/mcp/tools.test.ts +248 -0
- package/tests/parser/schema.test.ts +134 -0
- package/tests/parser/variables.test.ts +227 -0
- package/tests/parser/yaml-parser.test.ts +69 -0
- package/tests/reporter/console.test.ts +256 -0
- package/tests/reporter/json.test.ts +98 -0
- package/tests/reporter/junit.test.ts +284 -0
- package/tests/runner/assertions.test.ts +262 -0
- package/tests/runner/executor.test.ts +310 -0
- package/tests/runner/http-client.test.ts +138 -0
- package/tests/web/routes.test.ts +160 -0
- 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
|
+
}
|