@kirrosh/zond 0.12.4 → 0.12.7

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/README.md CHANGED
@@ -1,128 +1,83 @@
1
1
  # zond
2
2
 
3
- Point your AI agent at an OpenAPI spec. Get working tests in minutes. No config, no cloud, no Postman.
3
+ AI-powered API testing for Claude Code, Cursor, and CI/CD.
4
4
 
5
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=zond&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBraXJyb3NoL3pvbmQiLCJtY3AiXX0K)
5
+ Say "test my API" — get working tests, coverage dashboard, and CI config in minutes.
6
+
7
+ <!-- TODO: add demo GIF (15 sec: plugin install → "cover openapi.json with tests" → 42/47 endpoints covered → dashboard) -->
6
8
 
7
- ## Claude Code Plugin
9
+ Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: structured tools, safety guardrails, coverage tracking, and run history. You don't need to learn anything new — just describe what you want and the agent handles the rest.
8
10
 
9
- Install in Claude Code:
11
+ ## Quick Start
10
12
 
11
13
  ```
12
14
  /plugin marketplace add kirrosh/zond
13
15
  /plugin install zond@zond-marketplace
14
16
  ```
15
17
 
16
- This gives you:
17
- - **17 MCP tools** for API testing (test generation, execution, diagnostics, coverage)
18
- - **Skills** for test generation, debugging failures, and CI setup
19
- - **Slash commands**: `/zond:api-test`, `/zond:api-coverage`
20
-
21
- After installation, just say: _"Safely cover the API from openapi.json with tests"_ — the agent handles everything.
22
-
23
- ## Install
24
-
25
- ```bash
26
- # Option 1: via npx (recommended — works everywhere with Node.js)
27
- npx -y @kirrosh/zond --version
28
-
29
- # Option 2: Binary (no Node.js required)
30
- # macOS / Linux
31
- curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
32
-
33
- # Windows
34
- iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex
35
- ```
18
+ Then say: _"Safely cover the API from openapi.json with tests"_
36
19
 
37
- [All releases](https://github.com/kirrosh/zond/releases) (Linux x64, macOS ARM, Windows x64)
38
-
39
- ## MCP Setup (Cursor / Claude Code / Windsurf)
40
-
41
- Click the badge above, or add manually:
42
-
43
- ```json
44
- {
45
- "mcpServers": {
46
- "zond": {
47
- "command": "npx",
48
- "args": [
49
- "-y",
50
- "@kirrosh/zond@latest",
51
- "mcp",
52
- "--dir",
53
- "${workspaceFolder}"
54
- ]
55
- }
56
- }
57
- }
58
- ```
20
+ You get skills, slash commands, and 12 MCP tools in one package.
59
21
 
60
- > `@latest` ensures npx always pulls the newest version on each restart — no manual update needed.
22
+ <details>
23
+ <summary>Other installation methods (MCP, CLI, binary)</summary>
61
24
 
62
- **Where to put this:**
25
+ ### MCP Server (Cursor, Windsurf, other editors)
63
26
 
64
- | Editor | Config file |
65
- | ----------- | ----------------------------------------------------- |
66
- | Cursor | Settings > MCP, or `.cursor/mcp.json` in project root |
67
- | Claude Code | `.mcp.json` in project root |
68
- | Windsurf | `.windsurfrules/mcp.json` or settings |
27
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=zond&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBraXJyb3NoL3pvbmQiLCJtY3AiXX0K)
69
28
 
70
- ## Main Flow (5 steps)
29
+ Or add manually — see [MCP setup guide](docs/mcp-guide.md) for Cursor, Claude Code, and Windsurf config.
71
30
 
72
- Once MCP is connected, ask your AI agent to cover your API with tests:
31
+ ### CLI / Binary
73
32
 
74
- **1. Register your API**
33
+ ```bash
34
+ npx -y @kirrosh/zond --version
75
35
 
76
- ```
77
- setup_api(name: "myapi", specPath: "openapi.json")
36
+ # Standalone binary (no Node.js required)
37
+ curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
38
+ iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
78
39
  ```
79
40
 
80
- **2. Generate a test guide** (agent reads OpenAPI + gets instructions)
81
-
82
- ```
83
- generate_and_save(specPath: "openapi.json")
84
- ```
41
+ See [ZOND.md](ZOND.md) for full CLI reference.
85
42
 
86
- For large APIs (>30 endpoints), auto-chunks by tags and returns a plan. Call with `tag` for each chunk.
43
+ </details>
87
44
 
88
- **3. Save test suites** (agent writes YAML based on the guide)
45
+ ## What Happens
89
46
 
90
- ```
91
- save_test_suite(filePath: "apis/myapi/tests/smoke.yaml", content: "...")
92
- ```
47
+ 1. **Point** — you give the agent an OpenAPI spec
48
+ 2. **Generate** — zond reads the spec, produces YAML test suites (smoke + CRUD)
49
+ 3. **Run** — tests execute, failures are diagnosed, coverage is tracked
93
50
 
94
- **4. Run tests**
51
+ The agent does all three steps autonomously. It asks you only when it needs an auth token or permission to run write operations.
95
52
 
96
- ```
97
- run_tests(testPath: "apis/myapi/tests/", safe: true)
98
- ```
53
+ ## Why Not Just Ask Claude to Write pytest?
99
54
 
100
- **5. Diagnose failures**
55
+ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flow, has no safety guardrails, no coverage tracking, and no run history. Zond gives the agent structured tools to do it in 5 minutes with full visibility.
101
56
 
102
- ```
103
- query_db(action: "diagnose_failure", runId: 42)
104
- ```
57
+ ## Key Capabilities
105
58
 
106
- Or just say: _"Safely cover the API from openapi.json with tests"_ — the agent will do all 5 steps.
59
+ | | |
60
+ |---|---|
61
+ | **Safe by Default** | `--safe` runs only GET requests. `--dry-run` previews without sending. The agent never touches production data without your explicit approval. |
62
+ | **Spec-Grounded** | Tests are derived from your OpenAPI schema, not invented from scratch. The spec is the source of truth. |
63
+ | **Full Visibility** | Every run is stored in SQLite. Compare runs, track regressions, see exactly what the server returned. |
64
+ | **Coverage Tracking** | See which endpoints are tested, which aren't, and what broke since last run. |
65
+ | **CI-Ready** | One command generates GitHub Actions or GitLab CI workflow. Tests in YAML, in git, with code review. |
107
66
 
108
- ## CLI
67
+ ## Try It
109
68
 
110
69
  ```
111
- zond run <path> Run tests (--env, --safe, --tag, --dry-run, --env-var, --report)
112
- zond add-api <name> Register API (--spec <openapi>)
113
- zond coverage API test coverage (--spec, --tests, --fail-on-coverage)
114
- zond compare <runA> <runB> Compare two test runs
115
- zond serve Web dashboard with health strip + endpoints/suites/runs tabs (--port 8080)
116
- zond mcp Start MCP server
117
- zond chat AI chat agent (--provider ollama|openai|anthropic)
118
- zond doctor Diagnostics
70
+ "Cover openapi.json with tests"
71
+ "Run only smoke tests against staging"
72
+ "What broke since last run?"
73
+ "Set up CI for API tests"
119
74
  ```
120
75
 
121
76
  ## Documentation
122
77
 
123
- - [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart guide (RU)
124
78
  - [ZOND.md](ZOND.md) — full CLI and MCP tools reference
125
79
  - [docs/mcp-guide.md](docs/mcp-guide.md) — MCP agent workflow guide
80
+ - [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart (RU)
126
81
  - [docs/ci.md](docs/ci.md) — CI/CD integration
127
82
 
128
83
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.12.4",
3
+ "version": "0.12.7",
4
4
  "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
@@ -7,10 +7,11 @@ export interface AddApiOptions {
7
7
  dir?: string;
8
8
  envPairs?: string[];
9
9
  dbPath?: string;
10
+ insecure?: boolean;
10
11
  }
11
12
 
12
13
  export async function addApiCommand(options: AddApiOptions): Promise<number> {
13
- const { name, spec, envPairs, dbPath, dir } = options;
14
+ const { name, spec, envPairs, dbPath, dir, insecure } = options;
14
15
 
15
16
  // Parse --env key=value pairs into a record
16
17
  const envVars: Record<string, string> = {};
@@ -31,6 +32,7 @@ export async function addApiCommand(options: AddApiOptions): Promise<number> {
31
32
  dir,
32
33
  envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
33
34
  dbPath,
35
+ insecure,
34
36
  });
35
37
 
36
38
  printSuccess(`API '${name}' created (id=${result.collectionId})`);
package/src/cli/index.ts CHANGED
@@ -96,6 +96,7 @@ Options for 'add-api':
96
96
  --spec <path-or-url> OpenAPI spec (extracts base_url from servers[0])
97
97
  --dir <directory> Base directory (default: ./apis/<name>/)
98
98
  --env key=value Set environment variables (repeatable)
99
+ --insecure Skip TLS verification (self-signed certs)
99
100
 
100
101
  Options for 'chat':
101
102
  --provider <name> LLM provider: ollama, openai, anthropic, custom (default: ollama)
@@ -210,6 +211,7 @@ async function main(): Promise<number> {
210
211
  dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
211
212
  envPairs: envValues.length > 0 ? envValues : undefined,
212
213
  dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
214
+ insecure: flags["insecure"] === true,
213
215
  });
214
216
  }
215
217
 
@@ -106,15 +106,18 @@ Inline the value directly — there is NO \`params\` field:
106
106
  - Array element: \`items.0.name: { exists: true }\`
107
107
  - YAML keys must be unique — do NOT repeat \`_body\` twice
108
108
 
109
- ### Request body (JSON)
109
+ ### Request body — IMPORTANT
110
+ Use \`json:\` for JSON request bodies. Do NOT use \`body:\` — it is not a valid key.
110
111
  \`\`\`yaml
111
112
  - name: Create resource
112
113
  POST: /resources
113
- json: { name: "test", email: "a@b.com" }
114
+ json: { name: "test", email: "a@b.com" } # correct — use json:
115
+ # body: { ... } # WRONG — body: is not supported
114
116
  expect:
115
117
  status: 201
116
118
  id: { exists: true }
117
119
  \`\`\`
120
+ For form-encoded: use \`form:\` instead of \`json:\`.
118
121
 
119
122
  ### Built-in generators
120
123
  \`{{$uuid}}\`, \`{{$randomInt}}\`, \`{{$timestamp}}\`, \`{{$randomName}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`
@@ -140,6 +143,12 @@ Inline the value directly — there is NO \`params\` field:
140
143
  Use spec paths with \`{param}\` placeholders in the path for coverage to match:
141
144
  - Spec says \`GET /products/{id}\` → write \`GET: /products/1\` (hardcode the value)
142
145
  - Coverage scanner matches test paths against spec paths automatically
146
+
147
+ ### CRITICAL: Never mask server errors
148
+ - If an endpoint returns 500 — do NOT change expect to \`status: 500\`. Keep \`status: 200\` and let the test fail.
149
+ - A failing test = signal about an API bug. The goal is NOT "all tests green" but "tests reflect expected behavior".
150
+ - Fixing tests means fixing test logic (wrong path, missing auth, bad body), NOT accepting error responses as expected.
151
+ - Legitimate error expectations: 404 for missing resources, 400/422 for invalid input, 401 for no auth — these are negative tests by design.
143
152
  `;
144
153
 
145
154
  export interface GuideOptions {
@@ -4,10 +4,12 @@ import type { EndpointInfo, ResponseInfo, SecuritySchemeInfo } from "./types.ts"
4
4
 
5
5
  const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
6
6
 
7
- export async function readOpenApiSpec(specPath: string): Promise<OpenAPIV3.Document> {
7
+ export async function readOpenApiSpec(specPath: string, options?: { insecure?: boolean }): Promise<OpenAPIV3.Document> {
8
8
  // For HTTP URLs, fetch the spec first then dereference the parsed object
9
9
  if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
10
- const resp = await fetch(specPath);
10
+ const resp = await fetch(specPath, {
11
+ ...(options?.insecure ? { tls: { rejectUnauthorized: false } } : {}),
12
+ });
11
13
  if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
12
14
  const spec = await resp.json();
13
15
  const api = await dereference(spec as string);
@@ -62,10 +62,19 @@ function getAuthHeaders(
62
62
  const scheme = schemes.find(s => s.name === secName);
63
63
  if (!scheme) continue;
64
64
 
65
- if (scheme.type === "http" && scheme.scheme === "bearer") {
66
- return { Authorization: "Bearer {{auth_token}}" };
65
+ if (scheme.type === "http") {
66
+ if (scheme.scheme === "bearer" || !scheme.scheme) {
67
+ return { Authorization: "Bearer {{auth_token}}" };
68
+ }
69
+ if (scheme.scheme === "basic") {
70
+ return { Authorization: "Basic {{auth_token}}" };
71
+ }
67
72
  }
68
73
  if (scheme.type === "apiKey" && scheme.in === "header" && scheme.apiKeyName) {
74
+ // When apiKey scheme uses Authorization header, it's typically a Bearer token
75
+ if (scheme.apiKeyName === "Authorization") {
76
+ return { Authorization: "Bearer {{auth_token}}" };
77
+ }
69
78
  return { [scheme.apiKeyName]: "{{api_key}}" };
70
79
  }
71
80
  }
@@ -53,6 +53,16 @@ export async function executeRequest(
53
53
  // Body is not valid JSON despite content-type
54
54
  }
55
55
  }
56
+ // Fallback: for non-JSON responses, store trimmed body as string
57
+ // so that captures like `_body` work for text/plain, text/html, etc.
58
+ if (body_parsed === undefined && bodyText.length > 0) {
59
+ // Try JSON parse as fallback (some APIs omit content-type)
60
+ try {
61
+ body_parsed = JSON.parse(bodyText);
62
+ } catch {
63
+ body_parsed = bodyText.trim();
64
+ }
65
+ }
56
66
 
57
67
  const headers: Record<string, string> = {};
58
68
  response.headers.forEach((v, k) => {
@@ -20,6 +20,7 @@ export interface SetupApiOptions {
20
20
  envVars?: Record<string, string>;
21
21
  dbPath?: string;
22
22
  force?: boolean;
23
+ insecure?: boolean;
23
24
  }
24
25
 
25
26
  export interface SetupApiResult {
@@ -30,6 +31,7 @@ export interface SetupApiResult {
30
31
  baseUrl: string;
31
32
  specEndpoints: number;
32
33
  pathParams?: Record<string, string>;
34
+ warnings?: string[];
33
35
  }
34
36
 
35
37
  export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult> {
@@ -42,13 +44,17 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
42
44
  let baseUrl = "";
43
45
  let endpointCount = 0;
44
46
  const pathParams = new Map<string, string>();
47
+ const warnings: string[] = [];
45
48
  let specTitle: string | undefined;
46
49
  if (spec) {
47
- const doc = await readOpenApiSpec(spec);
50
+ const doc = await readOpenApiSpec(spec, { insecure: options.insecure });
48
51
  openapiSpec = spec;
49
52
  if ((doc as any).servers?.[0]?.url) {
50
53
  baseUrl = (doc as any).servers[0].url;
51
54
  }
55
+ if (baseUrl && !baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
56
+ warnings.push(`Spec server URL "${baseUrl}" is relative — requests will fail without a host. Override with envVars: {"base_url": "https://your-host${baseUrl}"}`);
57
+ }
52
58
  specTitle = (doc as any).info?.title;
53
59
  const endpoints = extractEndpoints(doc);
54
60
  endpointCount = endpoints.length;
@@ -139,5 +145,6 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
139
145
  baseUrl,
140
146
  specEndpoints: endpointCount,
141
147
  ...(pathParamsObj ? { pathParams: pathParamsObj } : {}),
148
+ ...(warnings.length > 0 ? { warnings } : {}),
142
149
  };
143
150
  }
@@ -12,7 +12,8 @@ export const TOOL_DESCRIPTIONS = {
12
12
  setup_api:
13
13
  "Register a new API for testing. Creates directory structure, reads OpenAPI spec, " +
14
14
  "sets up environment variables, and creates a collection in the database. " +
15
- "Use this before generating tests for a new API.",
15
+ "Use this before generating tests for a new API. " +
16
+ "Warns if spec has relative server URL. Use insecure: true for self-signed HTTPS certs.",
16
17
 
17
18
  describe_endpoint:
18
19
  "Full details for one endpoint: params grouped by type, request body schema, " +
@@ -36,7 +37,7 @@ export const TOOL_DESCRIPTIONS = {
36
37
  "Query the zond database. Actions: list_collections (all APIs with run stats), " +
37
38
  "list_runs (recent test runs), get_run_results (full detail for a run), " +
38
39
  "diagnose_failure (only failed/errored steps for a run — each failure includes failure_type: api_error/assertion_failed/network_error, " +
39
- "and summary includes api_errors/assertion_failures/network_errors counts), " +
40
+ "and summary includes api_errors/assertion_failures/network_errors counts; stack traces are truncated by default, use verbose: true for full traces), " +
40
41
  "compare_runs (regressions and fixes between two runs).",
41
42
 
42
43
  coverage_analysis:
@@ -46,7 +47,8 @@ export const TOOL_DESCRIPTIONS = {
46
47
  "Always includes static spec warnings (deprecated, missing response schemas, required params without examples).",
47
48
 
48
49
  send_request:
49
- "Send an ad-hoc HTTP request. Supports variable interpolation from environments (e.g. {{base_url}}).",
50
+ "Send an ad-hoc HTTP request. Supports variable interpolation from environments (e.g. {{base_url}}). " +
51
+ "Use jsonPath to extract a subset of the response (e.g. '[0].code'), maxResponseChars to truncate large responses.",
50
52
 
51
53
  manage_server:
52
54
  "Start, stop, restart, or check status of the zond WebUI server. " +
@@ -6,6 +6,28 @@ import { join } from "node:path";
6
6
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
7
  import { statusHint, classifyFailure, envHint, envCategory, schemaHint } from "../../core/diagnostics/failure-hints.ts";
8
8
 
9
+ function truncateErrorMessage(raw: string | null | undefined, verbose?: boolean): string | undefined {
10
+ if (!raw) return undefined;
11
+ if (verbose || raw.length < 500) return raw;
12
+ const lines = raw.split(/\r?\n/);
13
+ // First line is the error message itself
14
+ const msgLines = [lines[0]!];
15
+ // Grab up to 3 stack-trace lines (indented or starting with "at ")
16
+ let traceCount = 0;
17
+ for (let i = 1; i < lines.length && traceCount < 3; i++) {
18
+ const line = lines[i]!;
19
+ if (/^\s+/.test(line) || /^\s*at\s/.test(line)) {
20
+ msgLines.push(line);
21
+ traceCount++;
22
+ }
23
+ }
24
+ const remaining = lines.length - msgLines.length;
25
+ if (remaining > 0) {
26
+ msgLines.push(`...[truncated ${remaining} lines]`);
27
+ }
28
+ return msgLines.join("\n");
29
+ }
30
+
9
31
  function parseBodySafe(raw: string | null | undefined): unknown {
10
32
  if (!raw) return undefined;
11
33
  const truncated = raw.length > 2000 ? raw.slice(0, 2000) + "…[truncated]" : raw;
@@ -49,8 +71,10 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
49
71
  .describe("Second run ID (required for compare_runs — this is the newer run)"),
50
72
  limit: z.optional(z.number().int().min(1).max(100))
51
73
  .describe("Max number of runs to return (default: 20, only for list_runs)"),
74
+ verbose: z.optional(z.boolean())
75
+ .describe("Show full error messages and stack traces (default: false, truncates long traces)"),
52
76
  },
53
- }, async ({ action, runId, runIdB, limit }) => {
77
+ }, async ({ action, runId, runIdB, limit, verbose }) => {
54
78
  try {
55
79
  getDb(dbPath);
56
80
 
@@ -105,7 +129,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
105
129
  request_method: r.request_method,
106
130
  request_url: r.request_url,
107
131
  response_status: r.response_status,
108
- error_message: r.error_message,
132
+ error_message: truncateErrorMessage(r.error_message, verbose),
109
133
  assertions: r.assertions,
110
134
  })),
111
135
  };
@@ -151,7 +175,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
151
175
  test_name: r.test_name,
152
176
  status: r.status,
153
177
  failure_type,
154
- error_message: r.error_message,
178
+ error_message: truncateErrorMessage(r.error_message, verbose),
155
179
  request_method: r.request_method,
156
180
  request_url: r.request_url,
157
181
  response_status: r.response_status,
@@ -6,6 +6,24 @@ import { getDb } from "../../db/schema.ts";
6
6
  import { findCollectionByNameOrId } from "../../db/queries.ts";
7
7
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
8
8
 
9
+ function extractByPath(obj: unknown, path: string): unknown {
10
+ const segments = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
11
+ let current: unknown = obj;
12
+ for (const seg of segments) {
13
+ if (current === null || current === undefined) return undefined;
14
+ if (Array.isArray(current)) {
15
+ const idx = parseInt(seg, 10);
16
+ if (isNaN(idx)) return undefined;
17
+ current = current[idx];
18
+ } else if (typeof current === 'object') {
19
+ current = (current as Record<string, unknown>)[seg];
20
+ } else {
21
+ return undefined;
22
+ }
23
+ }
24
+ return current;
25
+ }
26
+
9
27
  export function registerSendRequestTool(server: McpServer, dbPath?: string) {
10
28
  server.registerTool("send_request", {
11
29
  description: TOOL_DESCRIPTIONS.send_request,
@@ -17,8 +35,10 @@ export function registerSendRequestTool(server: McpServer, dbPath?: string) {
17
35
  timeout: z.optional(z.number().int().positive()).describe("Request timeout in ms"),
18
36
  envName: z.optional(z.string()).describe("Environment name for variable interpolation"),
19
37
  collectionName: z.optional(z.string()).describe("Collection name to load env from its base_dir (e.g. 'petstore'). Required for {{variable}} interpolation."),
38
+ jsonPath: z.optional(z.string()).describe("Simple dot-notation path to extract from response body (e.g. '[0].code', 'data.items', 'id'). Supports array indices."),
39
+ maxResponseChars: z.optional(z.number().int().positive()).describe("Truncate response body to this many characters"),
20
40
  },
21
- }, async ({ method, url, headers, body, timeout, envName, collectionName }) => {
41
+ }, async ({ method, url, headers, body, timeout, envName, collectionName, jsonPath, maxResponseChars }) => {
22
42
  try {
23
43
  let searchDir = process.cwd();
24
44
  if (collectionName) {
@@ -43,15 +63,29 @@ export function registerSendRequestTool(server: McpServer, dbPath?: string) {
43
63
  timeout ? { timeout } : undefined,
44
64
  );
45
65
 
66
+ let responseBody: unknown = response.body_parsed ?? response.body;
67
+
68
+ // Apply jsonPath filter
69
+ if (jsonPath && responseBody !== undefined) {
70
+ responseBody = extractByPath(responseBody, jsonPath);
71
+ }
72
+
46
73
  const result = {
47
74
  status: response.status,
48
75
  headers: response.headers,
49
- body: response.body_parsed ?? response.body,
76
+ body: responseBody,
50
77
  duration_ms: response.duration_ms,
51
78
  };
52
79
 
80
+ let text = JSON.stringify(result, null, 2);
81
+
82
+ // Apply maxResponseChars truncation
83
+ if (maxResponseChars && text.length > maxResponseChars) {
84
+ text = text.slice(0, maxResponseChars) + '\n…[truncated]';
85
+ }
86
+
53
87
  return {
54
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
88
+ content: [{ type: "text" as const, text }],
55
89
  };
56
90
  } catch (err) {
57
91
  return {
@@ -27,8 +27,9 @@ export function registerSetupApiTool(server: McpServer, dbPath?: string) {
27
27
  dir: z.optional(z.string()).describe("Base directory (default: ./apis/<name>/)"),
28
28
  envVars: z.optional(z.string()).describe("Environment variables as JSON string (e.g. '{\"base_url\": \"...\", \"token\": \"...\"}')"),
29
29
  force: z.optional(z.boolean()).describe("If true, delete existing API with same name and recreate from scratch"),
30
+ insecure: z.optional(z.boolean()).describe("Skip TLS certificate verification when fetching spec over HTTPS (for self-signed certs)"),
30
31
  },
31
- }, async ({ name, specPath, dir, envVars, force }) => {
32
+ }, async ({ name, specPath, dir, envVars, force, insecure }) => {
32
33
  try {
33
34
  let parsedEnvVars: Record<string, string> | undefined;
34
35
  if (envVars) {
@@ -59,12 +60,15 @@ export function registerSetupApiTool(server: McpServer, dbPath?: string) {
59
60
  envVars: parsedEnvVars,
60
61
  dbPath,
61
62
  force,
63
+ insecure,
62
64
  });
63
65
 
64
66
  const envFilePath = join(result.baseDir, ".env.yaml");
67
+ const warningSteps = result.warnings?.map(w => `WARNING: ${w}`) ?? [];
65
68
  const response = {
66
69
  ...result,
67
70
  nextSteps: [
71
+ ...warningSteps,
68
72
  `Edit ${envFilePath} to add credentials (auth_token, api_key, base_url, etc.)`,
69
73
  `File is already git-ignored via .gitignore`,
70
74
  `Then run: run_tests(testPath: "${result.testPath}")`,