@kirrosh/zond 0.14.0 → 0.17.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.
- package/CHANGELOG.md +132 -112
- package/README.md +3 -10
- package/package.json +4 -4
- package/src/cli/commands/ci-init.ts +12 -1
- package/src/cli/commands/coverage.ts +21 -1
- package/src/cli/commands/db.ts +121 -0
- package/src/cli/commands/describe.ts +60 -0
- package/src/cli/commands/export.ts +144 -0
- package/src/cli/commands/generate.ts +158 -0
- package/src/cli/commands/guide.ts +127 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/request.ts +57 -0
- package/src/cli/commands/run.ts +74 -14
- package/src/cli/commands/serve.ts +62 -3
- package/src/cli/commands/sync.ts +240 -0
- package/src/cli/commands/validate.ts +18 -2
- package/src/cli/index.ts +258 -17
- package/src/cli/json-envelope.ts +19 -0
- package/src/core/diagnostics/db-analysis.ts +423 -0
- package/src/core/diagnostics/failure-hints.ts +40 -0
- package/src/core/exporter/postman.ts +963 -0
- package/src/core/generator/data-factory.ts +55 -9
- package/src/core/generator/describe.ts +250 -0
- package/src/core/generator/guide-builder.ts +20 -0
- package/src/core/generator/index.ts +1 -1
- package/src/core/generator/openapi-reader.ts +6 -0
- package/src/core/generator/serializer.ts +17 -2
- package/src/core/generator/suite-generator.ts +291 -29
- package/src/core/generator/types.ts +1 -0
- package/src/core/meta/meta-store.ts +78 -0
- package/src/core/meta/types.ts +21 -0
- package/src/core/parser/schema.ts +12 -2
- package/src/core/parser/types.ts +12 -1
- package/src/core/parser/variables.ts +3 -0
- package/src/core/parser/yaml-parser.ts +2 -1
- package/src/core/runner/assertions.ts +44 -20
- package/src/core/runner/execute-run.ts +31 -8
- package/src/core/runner/executor.ts +35 -8
- package/src/core/runner/http-client.ts +1 -1
- package/src/core/runner/send-request.ts +94 -0
- package/src/core/runner/types.ts +2 -0
- package/src/core/sync/spec-differ.ts +38 -0
- package/src/db/queries.ts +4 -2
- package/src/db/schema.ts +11 -3
- package/src/web/views/suites-tab.ts +1 -1
- package/src/cli/commands/mcp.ts +0 -16
- package/src/mcp/descriptions.ts +0 -71
- package/src/mcp/server.ts +0 -45
- package/src/mcp/tools/ci-init.ts +0 -54
- package/src/mcp/tools/coverage-analysis.ts +0 -141
- package/src/mcp/tools/describe-endpoint.ts +0 -242
- package/src/mcp/tools/generate-and-save.ts +0 -202
- package/src/mcp/tools/manage-server.ts +0 -86
- package/src/mcp/tools/query-db.ts +0 -300
- package/src/mcp/tools/run-tests.ts +0 -115
- package/src/mcp/tools/save-test-suite.ts +0 -218
- package/src/mcp/tools/send-request.ts +0 -97
- package/src/mcp/tools/set-work-dir.ts +0 -35
- package/src/mcp/tools/setup-api.ts +0 -88
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { validateSuite } from "../../core/parser/schema.ts";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
5
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
-
import YAML from "yaml";
|
|
7
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
8
|
-
|
|
9
|
-
export interface SaveResult {
|
|
10
|
-
saved: boolean;
|
|
11
|
-
filePath?: string;
|
|
12
|
-
suite?: { name: unknown; tests: number; base_url: unknown };
|
|
13
|
-
hint?: string;
|
|
14
|
-
coverage?: Record<string, unknown>;
|
|
15
|
-
error?: string;
|
|
16
|
-
detected?: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function validateAndSave(
|
|
20
|
-
filePath: string,
|
|
21
|
-
content: string,
|
|
22
|
-
overwrite: boolean | undefined,
|
|
23
|
-
dbPath?: string,
|
|
24
|
-
): Promise<{ result: SaveResult; isError: boolean }> {
|
|
25
|
-
// Parse YAML
|
|
26
|
-
let parsed: unknown;
|
|
27
|
-
try {
|
|
28
|
-
parsed = YAML.parse(content);
|
|
29
|
-
} catch (err) {
|
|
30
|
-
return {
|
|
31
|
-
result: {
|
|
32
|
-
saved: false,
|
|
33
|
-
error: `YAML parse error: ${(err as Error).message}`,
|
|
34
|
-
hint: "Check YAML syntax — indentation, colons, and quoting",
|
|
35
|
-
},
|
|
36
|
-
isError: true,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Validate against test suite schema
|
|
41
|
-
try {
|
|
42
|
-
validateSuite(parsed);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
const message = (err as Error).message;
|
|
45
|
-
let hint = "Check the test suite structure matches the expected format";
|
|
46
|
-
if (message.includes("status")) {
|
|
47
|
-
hint = "Status codes must be numbers, not strings (e.g. status: 200, not status: \"200\")";
|
|
48
|
-
} else if (message.includes("exists")) {
|
|
49
|
-
hint = "exists must be boolean true/false, not string \"true\"/\"false\"";
|
|
50
|
-
} else if (message.includes("tests")) {
|
|
51
|
-
hint = "Suite must have a 'tests' array with at least one test step";
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
result: { saved: false, error: `Validation: ${message}`, hint },
|
|
55
|
-
isError: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Detect hardcoded credentials — long opaque strings in auth headers
|
|
60
|
-
const credentialPattern = /Authorization\s*:\s*["']?(Basic|Bearer)\s+([A-Za-z0-9+/=_\-]{20,})["']?/g;
|
|
61
|
-
const credMatches = [...content.matchAll(credentialPattern)];
|
|
62
|
-
const suspiciousCredentials = credMatches.filter(m => {
|
|
63
|
-
const value = m[2]!;
|
|
64
|
-
return !value.startsWith("{{") && !value.endsWith("}}");
|
|
65
|
-
});
|
|
66
|
-
if (suspiciousCredentials.length > 0) {
|
|
67
|
-
return {
|
|
68
|
-
result: {
|
|
69
|
-
saved: false,
|
|
70
|
-
error: "Hardcoded credentials detected in Authorization header(s)",
|
|
71
|
-
hint: "Never put literal API keys or tokens in YAML files. Store them in the .env.yaml file in the API directory and reference as {{api_key}} in headers.",
|
|
72
|
-
detected: suspiciousCredentials.map(m => `${m[1]} <redacted>`),
|
|
73
|
-
},
|
|
74
|
-
isError: true,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Resolve path
|
|
79
|
-
const resolvedPath = filePath.startsWith("/") || /^[a-zA-Z]:/.test(filePath)
|
|
80
|
-
? filePath
|
|
81
|
-
: join(process.cwd(), filePath);
|
|
82
|
-
|
|
83
|
-
// Check existing file
|
|
84
|
-
if (!overwrite && existsSync(resolvedPath)) {
|
|
85
|
-
return {
|
|
86
|
-
result: {
|
|
87
|
-
saved: false,
|
|
88
|
-
error: `File already exists: ${resolvedPath}`,
|
|
89
|
-
hint: "Use overwrite: true to replace the existing file",
|
|
90
|
-
},
|
|
91
|
-
isError: true,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Create directories
|
|
96
|
-
const dir = dirname(resolvedPath);
|
|
97
|
-
if (!existsSync(dir)) {
|
|
98
|
-
mkdirSync(dir, { recursive: true });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Write original YAML content (preserve formatting/comments)
|
|
102
|
-
await Bun.write(resolvedPath, content);
|
|
103
|
-
|
|
104
|
-
// Extract summary info
|
|
105
|
-
const suite = parsed as Record<string, unknown>;
|
|
106
|
-
const tests = (suite.tests as unknown[]) ?? [];
|
|
107
|
-
|
|
108
|
-
const result: SaveResult = {
|
|
109
|
-
saved: true,
|
|
110
|
-
filePath: resolvedPath,
|
|
111
|
-
suite: {
|
|
112
|
-
name: suite.name,
|
|
113
|
-
tests: tests.length,
|
|
114
|
-
base_url: suite.base_url ?? null,
|
|
115
|
-
},
|
|
116
|
-
hint: "After tests are ready, ask the user if they want to set up CI/CD with ci_init to run tests automatically on push.",
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Attempt to compute coverage hint
|
|
120
|
-
try {
|
|
121
|
-
const testDir = dirname(resolvedPath);
|
|
122
|
-
const { findCollectionByTestPath } = await import("../../db/queries.ts");
|
|
123
|
-
const { getDb } = await import("../../db/schema.ts");
|
|
124
|
-
getDb(dbPath);
|
|
125
|
-
const collection = findCollectionByTestPath(testDir);
|
|
126
|
-
if (collection?.openapi_spec) {
|
|
127
|
-
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
128
|
-
const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
|
|
129
|
-
|
|
130
|
-
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
131
|
-
const allEndpoints = extractEndpoints(doc);
|
|
132
|
-
const covered = await scanCoveredEndpoints(testDir);
|
|
133
|
-
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
134
|
-
|
|
135
|
-
const total = allEndpoints.length;
|
|
136
|
-
const coveredCount = total - uncovered.length;
|
|
137
|
-
const percentage = total > 0 ? Math.round((coveredCount / total) * 100) : 0;
|
|
138
|
-
|
|
139
|
-
const coverage: Record<string, unknown> = { percentage, covered: coveredCount, total, uncoveredCount: uncovered.length };
|
|
140
|
-
if (percentage < 80 && uncovered.length > 0) {
|
|
141
|
-
coverage.suggestion = `Use generate_and_save with testsDir to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
|
|
142
|
-
}
|
|
143
|
-
result.coverage = coverage;
|
|
144
|
-
}
|
|
145
|
-
} catch { /* silently skip coverage if unavailable */ }
|
|
146
|
-
|
|
147
|
-
return { result, isError: false };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function registerSaveTestSuiteTool(server: McpServer, dbPath?: string) {
|
|
151
|
-
server.registerTool("save_test_suite", {
|
|
152
|
-
description: TOOL_DESCRIPTIONS.save_test_suite,
|
|
153
|
-
inputSchema: {
|
|
154
|
-
filePath: z.string().describe("Path for saving the YAML test file (e.g. apis/petstore/tests/pets-crud.yaml)"),
|
|
155
|
-
content: z.string().describe("YAML content of the test suite"),
|
|
156
|
-
overwrite: z.optional(z.boolean()).describe("Overwrite existing file (default: false)"),
|
|
157
|
-
},
|
|
158
|
-
}, async ({ filePath, content, overwrite }) => {
|
|
159
|
-
try {
|
|
160
|
-
const { result, isError } = await validateAndSave(filePath, content, overwrite, dbPath);
|
|
161
|
-
return {
|
|
162
|
-
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
163
|
-
...(isError ? { isError: true } : {}),
|
|
164
|
-
};
|
|
165
|
-
} catch (err) {
|
|
166
|
-
return {
|
|
167
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
168
|
-
saved: false,
|
|
169
|
-
error: (err as Error).message,
|
|
170
|
-
}, null, 2) }],
|
|
171
|
-
isError: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function registerSaveTestSuitesTool(server: McpServer, dbPath?: string) {
|
|
178
|
-
server.registerTool("save_test_suites", {
|
|
179
|
-
description: TOOL_DESCRIPTIONS.save_test_suites,
|
|
180
|
-
inputSchema: {
|
|
181
|
-
files: z.array(z.object({
|
|
182
|
-
filePath: z.string().describe("Path for saving the YAML test file"),
|
|
183
|
-
content: z.string().describe("YAML content of the test suite"),
|
|
184
|
-
})).describe("Array of files to save"),
|
|
185
|
-
overwrite: z.optional(z.boolean()).describe("Overwrite existing files (default: false)"),
|
|
186
|
-
},
|
|
187
|
-
}, async ({ files, overwrite }) => {
|
|
188
|
-
try {
|
|
189
|
-
const results: Array<SaveResult & { filePath: string; inputPath: string }> = [];
|
|
190
|
-
let hasErrors = false;
|
|
191
|
-
|
|
192
|
-
for (const file of files) {
|
|
193
|
-
const { result, isError } = await validateAndSave(file.filePath, file.content, overwrite, dbPath);
|
|
194
|
-
results.push({ ...result, inputPath: file.filePath, filePath: result.filePath ?? file.filePath });
|
|
195
|
-
if (isError) hasErrors = true;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const summary = {
|
|
199
|
-
total: files.length,
|
|
200
|
-
saved: results.filter(r => r.saved).length,
|
|
201
|
-
failed: results.filter(r => !r.saved).length,
|
|
202
|
-
files: results,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }],
|
|
207
|
-
...(hasErrors ? { isError: true } : {}),
|
|
208
|
-
};
|
|
209
|
-
} catch (err) {
|
|
210
|
-
return {
|
|
211
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
212
|
-
error: (err as Error).message,
|
|
213
|
-
}, null, 2) }],
|
|
214
|
-
isError: true,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { executeRequest } from "../../core/runner/http-client.ts";
|
|
4
|
-
import { loadEnvironment, substituteString, substituteDeep } from "../../core/parser/variables.ts";
|
|
5
|
-
import { getDb } from "../../db/schema.ts";
|
|
6
|
-
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
7
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
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
|
-
|
|
27
|
-
export function registerSendRequestTool(server: McpServer, dbPath?: string) {
|
|
28
|
-
server.registerTool("send_request", {
|
|
29
|
-
description: TOOL_DESCRIPTIONS.send_request,
|
|
30
|
-
inputSchema: {
|
|
31
|
-
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
|
|
32
|
-
url: z.string().describe("Request URL (supports {{variable}} interpolation)"),
|
|
33
|
-
headers: z.optional(z.string()).describe("Request headers as JSON string (e.g. '{\"Content-Type\": \"application/json\"}')"),
|
|
34
|
-
body: z.optional(z.string()).describe("Request body (JSON string)"),
|
|
35
|
-
timeout: z.optional(z.number().int().positive()).describe("Request timeout in ms"),
|
|
36
|
-
envName: z.optional(z.string()).describe("Environment name for variable interpolation"),
|
|
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"),
|
|
40
|
-
},
|
|
41
|
-
}, async ({ method, url, headers, body, timeout, envName, collectionName, jsonPath, maxResponseChars }) => {
|
|
42
|
-
try {
|
|
43
|
-
let searchDir = process.cwd();
|
|
44
|
-
if (collectionName) {
|
|
45
|
-
getDb(dbPath);
|
|
46
|
-
const col = findCollectionByNameOrId(collectionName);
|
|
47
|
-
if (col?.base_dir) searchDir = col.base_dir;
|
|
48
|
-
}
|
|
49
|
-
const vars = await loadEnvironment(envName, searchDir);
|
|
50
|
-
|
|
51
|
-
const resolvedUrl = substituteString(url, vars) as string;
|
|
52
|
-
const parsedHeaders = headers ? JSON.parse(headers) as Record<string, string> : {};
|
|
53
|
-
const resolvedHeaders = Object.keys(parsedHeaders).length > 0 ? substituteDeep(parsedHeaders, vars) : {};
|
|
54
|
-
const resolvedBody = body ? substituteString(body, vars) as string : undefined;
|
|
55
|
-
|
|
56
|
-
const response = await executeRequest(
|
|
57
|
-
{
|
|
58
|
-
method,
|
|
59
|
-
url: resolvedUrl,
|
|
60
|
-
headers: resolvedHeaders,
|
|
61
|
-
body: resolvedBody,
|
|
62
|
-
},
|
|
63
|
-
timeout ? { timeout } : undefined,
|
|
64
|
-
);
|
|
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
|
-
|
|
73
|
-
const result = {
|
|
74
|
-
status: response.status,
|
|
75
|
-
headers: response.headers,
|
|
76
|
-
body: responseBody,
|
|
77
|
-
duration_ms: response.duration_ms,
|
|
78
|
-
};
|
|
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
|
-
|
|
87
|
-
return {
|
|
88
|
-
content: [{ type: "text" as const, text }],
|
|
89
|
-
};
|
|
90
|
-
} catch (err) {
|
|
91
|
-
return {
|
|
92
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
93
|
-
isError: true,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { resolve, join } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { resetDb } from "../../db/schema.ts";
|
|
6
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
7
|
-
|
|
8
|
-
export function registerSetWorkDirTool(server: McpServer) {
|
|
9
|
-
server.registerTool("set_work_dir", {
|
|
10
|
-
description: TOOL_DESCRIPTIONS.set_work_dir,
|
|
11
|
-
inputSchema: {
|
|
12
|
-
workDir: z.string().describe(
|
|
13
|
-
"Absolute path to project root (e.g. /home/user/myproject or C:/Users/user/myproject)"
|
|
14
|
-
),
|
|
15
|
-
},
|
|
16
|
-
}, async ({ workDir }) => {
|
|
17
|
-
const resolved = resolve(workDir);
|
|
18
|
-
if (!existsSync(resolved)) {
|
|
19
|
-
return {
|
|
20
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: `Directory not found: ${resolved}` }, null, 2) }],
|
|
21
|
-
isError: true,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
process.chdir(resolved);
|
|
25
|
-
resetDb();
|
|
26
|
-
const dbPath = join(resolved, "zond.db");
|
|
27
|
-
return {
|
|
28
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
29
|
-
workDir: resolved,
|
|
30
|
-
zond_db: dbPath,
|
|
31
|
-
hint: "Working directory set. All relative paths and zond.db will now resolve from this directory.",
|
|
32
|
-
}, null, 2) }],
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { setupApi } from "../../core/setup-api.ts";
|
|
6
|
-
import { resetDb } from "../../db/schema.ts";
|
|
7
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
8
|
-
|
|
9
|
-
function findProjectRoot(fromPath: string): string | null {
|
|
10
|
-
let current = existsSync(fromPath) ? fromPath : dirname(fromPath);
|
|
11
|
-
while (true) {
|
|
12
|
-
if (existsSync(join(current, ".git")) || existsSync(join(current, "package.json"))) {
|
|
13
|
-
return current;
|
|
14
|
-
}
|
|
15
|
-
const parent = dirname(current);
|
|
16
|
-
if (parent === current) return null;
|
|
17
|
-
current = parent;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function registerSetupApiTool(server: McpServer, dbPath?: string) {
|
|
22
|
-
server.registerTool("setup_api", {
|
|
23
|
-
description: TOOL_DESCRIPTIONS.setup_api,
|
|
24
|
-
inputSchema: {
|
|
25
|
-
name: z.optional(z.string()).describe("API name (auto-detected from spec title if omitted)"),
|
|
26
|
-
specPath: z.optional(z.string()).describe("Path or URL to OpenAPI spec"),
|
|
27
|
-
dir: z.optional(z.string()).describe("Base directory (default: ./apis/<name>/)"),
|
|
28
|
-
envVars: z.optional(z.string()).describe("Environment variables as JSON string (e.g. '{\"base_url\": \"...\", \"token\": \"...\"}')"),
|
|
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)"),
|
|
31
|
-
},
|
|
32
|
-
}, async ({ name, specPath, dir, envVars, force, insecure }) => {
|
|
33
|
-
try {
|
|
34
|
-
let parsedEnvVars: Record<string, string> | undefined;
|
|
35
|
-
if (envVars) {
|
|
36
|
-
try {
|
|
37
|
-
parsedEnvVars = JSON.parse(envVars);
|
|
38
|
-
} catch {
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: "envVars must be a valid JSON string" }, null, 2) }],
|
|
41
|
-
isError: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Auto-chdir to project root when dir is an absolute path
|
|
47
|
-
if (dir && isAbsolute(resolve(dir))) {
|
|
48
|
-
const resolvedDir = resolve(dir);
|
|
49
|
-
const root = findProjectRoot(resolvedDir);
|
|
50
|
-
if (root && root !== process.cwd()) {
|
|
51
|
-
process.chdir(root);
|
|
52
|
-
resetDb();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const result = await setupApi({
|
|
57
|
-
name,
|
|
58
|
-
spec: specPath,
|
|
59
|
-
dir,
|
|
60
|
-
envVars: parsedEnvVars,
|
|
61
|
-
dbPath,
|
|
62
|
-
force,
|
|
63
|
-
insecure,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const envFilePath = join(result.baseDir, ".env.yaml");
|
|
67
|
-
const warningSteps = result.warnings?.map(w => `WARNING: ${w}`) ?? [];
|
|
68
|
-
const response = {
|
|
69
|
-
...result,
|
|
70
|
-
nextSteps: [
|
|
71
|
-
...warningSteps,
|
|
72
|
-
`Edit ${envFilePath} to add credentials (auth_token, api_key, base_url, etc.)`,
|
|
73
|
-
`File is already git-ignored via .gitignore`,
|
|
74
|
-
`Then run: run_tests(testPath: "${result.testPath}")`,
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
80
|
-
};
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|