@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
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
|
|
5
|
+
let _db: Database | null = null;
|
|
6
|
+
let _dbPath: string | null = null;
|
|
7
|
+
|
|
8
|
+
export function getDb(dbPath?: string): Database {
|
|
9
|
+
const path = dbPath ? resolve(dbPath) : (_dbPath ?? resolve(process.cwd(), "apitool.db"));
|
|
10
|
+
|
|
11
|
+
// If cached connection exists, verify the file still exists
|
|
12
|
+
if (_db && _dbPath === path && existsSync(path)) return _db;
|
|
13
|
+
|
|
14
|
+
// Close stale connection if any
|
|
15
|
+
if (_db) {
|
|
16
|
+
try { _db.close(); } catch {}
|
|
17
|
+
_db = null;
|
|
18
|
+
_dbPath = null;
|
|
19
|
+
}
|
|
20
|
+
const db = new Database(path, { create: true });
|
|
21
|
+
|
|
22
|
+
// Performance and integrity settings
|
|
23
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
24
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
25
|
+
|
|
26
|
+
runMigrations(db);
|
|
27
|
+
|
|
28
|
+
_db = db;
|
|
29
|
+
_dbPath = path;
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function closeDb(): void {
|
|
34
|
+
if (_db) {
|
|
35
|
+
try { _db.close(); } catch {}
|
|
36
|
+
_db = null;
|
|
37
|
+
_dbPath = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ──────────────────────────────────────────────
|
|
42
|
+
// Schema
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const SCHEMA_VERSION = 6;
|
|
46
|
+
|
|
47
|
+
const SCHEMA_V1 = `
|
|
48
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
started_at TEXT NOT NULL,
|
|
51
|
+
finished_at TEXT,
|
|
52
|
+
total INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
passed INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
trigger TEXT DEFAULT 'manual',
|
|
57
|
+
commit_sha TEXT,
|
|
58
|
+
branch TEXT,
|
|
59
|
+
environment TEXT,
|
|
60
|
+
duration_ms INTEGER,
|
|
61
|
+
collection_id INTEGER REFERENCES collections(id)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS results (
|
|
65
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
|
+
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
67
|
+
suite_name TEXT NOT NULL,
|
|
68
|
+
test_name TEXT NOT NULL,
|
|
69
|
+
status TEXT NOT NULL,
|
|
70
|
+
duration_ms INTEGER NOT NULL,
|
|
71
|
+
request_method TEXT,
|
|
72
|
+
request_url TEXT,
|
|
73
|
+
request_body TEXT,
|
|
74
|
+
response_status INTEGER,
|
|
75
|
+
response_body TEXT,
|
|
76
|
+
error_message TEXT,
|
|
77
|
+
assertions TEXT,
|
|
78
|
+
captures TEXT
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
name TEXT NOT NULL UNIQUE,
|
|
84
|
+
variables TEXT NOT NULL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
88
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
+
name TEXT NOT NULL,
|
|
90
|
+
test_path TEXT NOT NULL,
|
|
91
|
+
openapi_spec TEXT,
|
|
92
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_runs_collection ON runs(collection_id);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_results_run ON results(run_id);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_results_status ON results(status);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_results_name ON results(suite_name, test_name);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_collections_name ON collections(name);
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const SCHEMA_V2 = `
|
|
104
|
+
CREATE TABLE IF NOT EXISTS ai_generations (
|
|
105
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
106
|
+
collection_id INTEGER REFERENCES collections(id),
|
|
107
|
+
prompt TEXT NOT NULL,
|
|
108
|
+
model TEXT NOT NULL,
|
|
109
|
+
provider TEXT NOT NULL,
|
|
110
|
+
generated_yaml TEXT,
|
|
111
|
+
output_path TEXT,
|
|
112
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
113
|
+
error_message TEXT,
|
|
114
|
+
prompt_tokens INTEGER,
|
|
115
|
+
completion_tokens INTEGER,
|
|
116
|
+
duration_ms INTEGER,
|
|
117
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
118
|
+
);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_ai_gen_collection ON ai_generations(collection_id);
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const SCHEMA_V3 = `
|
|
123
|
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
title TEXT,
|
|
126
|
+
provider TEXT NOT NULL,
|
|
127
|
+
model TEXT NOT NULL,
|
|
128
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
129
|
+
last_active TEXT NOT NULL DEFAULT (datetime('now'))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
133
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
134
|
+
session_id INTEGER NOT NULL REFERENCES chat_sessions(id),
|
|
135
|
+
role TEXT NOT NULL,
|
|
136
|
+
content TEXT NOT NULL,
|
|
137
|
+
tool_name TEXT,
|
|
138
|
+
tool_args TEXT,
|
|
139
|
+
tool_result TEXT,
|
|
140
|
+
input_tokens INTEGER,
|
|
141
|
+
output_tokens INTEGER,
|
|
142
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_chat_sessions_active ON chat_sessions(last_active DESC);
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
const SCHEMA_V4 = `
|
|
150
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
151
|
+
key TEXT PRIMARY KEY,
|
|
152
|
+
value TEXT NOT NULL
|
|
153
|
+
);
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const SCHEMA_V5 = `
|
|
157
|
+
ALTER TABLE collections ADD COLUMN base_dir TEXT;
|
|
158
|
+
|
|
159
|
+
CREATE TABLE environments_new (
|
|
160
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
161
|
+
name TEXT NOT NULL,
|
|
162
|
+
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
|
163
|
+
variables TEXT NOT NULL
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
INSERT INTO environments_new (id, name, collection_id, variables)
|
|
167
|
+
SELECT id, name, NULL, variables FROM environments;
|
|
168
|
+
|
|
169
|
+
DROP TABLE environments;
|
|
170
|
+
ALTER TABLE environments_new RENAME TO environments;
|
|
171
|
+
|
|
172
|
+
CREATE UNIQUE INDEX idx_env_name_collection ON environments(name, collection_id);
|
|
173
|
+
CREATE UNIQUE INDEX idx_env_name_global ON environments(name) WHERE collection_id IS NULL;
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const SCHEMA_V6 = `
|
|
177
|
+
ALTER TABLE results ADD COLUMN response_headers TEXT;
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
function runMigrations(db: Database): void {
|
|
181
|
+
const currentVersion = (db.query("PRAGMA user_version").get() as { user_version: number }).user_version;
|
|
182
|
+
|
|
183
|
+
if (currentVersion >= SCHEMA_VERSION) return;
|
|
184
|
+
|
|
185
|
+
db.transaction(() => {
|
|
186
|
+
if (currentVersion < 1) {
|
|
187
|
+
db.exec(SCHEMA_V1);
|
|
188
|
+
}
|
|
189
|
+
if (currentVersion < 2) {
|
|
190
|
+
db.exec(SCHEMA_V2);
|
|
191
|
+
}
|
|
192
|
+
if (currentVersion < 3) {
|
|
193
|
+
db.exec(SCHEMA_V3);
|
|
194
|
+
}
|
|
195
|
+
if (currentVersion < 4) {
|
|
196
|
+
db.exec(SCHEMA_V4);
|
|
197
|
+
}
|
|
198
|
+
if (currentVersion < 5) {
|
|
199
|
+
db.exec(SCHEMA_V5);
|
|
200
|
+
// Backfill base_dir from dirname(test_path) for existing collections
|
|
201
|
+
const rows = db.query("SELECT id, test_path FROM collections WHERE base_dir IS NULL").all() as { id: number; test_path: string }[];
|
|
202
|
+
const updateStmt = db.prepare("UPDATE collections SET base_dir = ? WHERE id = ?");
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
// test_path uses forward slashes (normalizePath); get parent dir
|
|
205
|
+
const lastSlash = row.test_path.lastIndexOf("/");
|
|
206
|
+
const baseDir = lastSlash > 0 ? row.test_path.slice(0, lastSlash) : row.test_path;
|
|
207
|
+
updateStmt.run(baseDir, row.id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (currentVersion < 6) {
|
|
211
|
+
db.exec(SCHEMA_V6);
|
|
212
|
+
}
|
|
213
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
214
|
+
})();
|
|
215
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerRunTestsTool } from "./tools/run-tests.ts";
|
|
4
|
+
import { registerValidateTestsTool } from "./tools/validate-tests.ts";
|
|
5
|
+
import { registerQueryDbTool } from "./tools/query-db.ts";
|
|
6
|
+
import { registerSendRequestTool } from "./tools/send-request.ts";
|
|
7
|
+
import { registerExploreApiTool } from "./tools/explore-api.ts";
|
|
8
|
+
import { registerManageEnvironmentTool } from "./tools/manage-environment.ts";
|
|
9
|
+
import { registerCoverageAnalysisTool } from "./tools/coverage-analysis.ts";
|
|
10
|
+
import { registerSaveTestSuiteTool } from "./tools/save-test-suite.ts";
|
|
11
|
+
import { registerGenerateTestsGuideTool } from "./tools/generate-tests-guide.ts";
|
|
12
|
+
import { registerSetupApiTool } from "./tools/setup-api.ts";
|
|
13
|
+
import { registerGenerateMissingTestsTool } from "./tools/generate-missing-tests.ts";
|
|
14
|
+
import { registerManageServerTool } from "./tools/manage-server.ts";
|
|
15
|
+
import { registerCiInitTool } from "./tools/ci-init.ts";
|
|
16
|
+
|
|
17
|
+
export interface McpServerOptions {
|
|
18
|
+
dbPath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function startMcpServer(options: McpServerOptions = {}): Promise<void> {
|
|
22
|
+
const { dbPath } = options;
|
|
23
|
+
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: "apitool",
|
|
26
|
+
version: "0.4.0",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Register all tools
|
|
30
|
+
registerRunTestsTool(server, dbPath);
|
|
31
|
+
registerValidateTestsTool(server);
|
|
32
|
+
registerQueryDbTool(server, dbPath);
|
|
33
|
+
registerSendRequestTool(server);
|
|
34
|
+
registerExploreApiTool(server);
|
|
35
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
36
|
+
registerCoverageAnalysisTool(server);
|
|
37
|
+
registerSaveTestSuiteTool(server, dbPath);
|
|
38
|
+
registerGenerateTestsGuideTool(server);
|
|
39
|
+
registerSetupApiTool(server, dbPath);
|
|
40
|
+
registerGenerateMissingTestsTool(server);
|
|
41
|
+
registerManageServerTool(server, dbPath);
|
|
42
|
+
registerCiInitTool(server);
|
|
43
|
+
|
|
44
|
+
// Connect via stdio transport
|
|
45
|
+
const transport = new StdioServerTransport();
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { ciInitCommand } from "../../cli/commands/ci-init.ts";
|
|
4
|
+
|
|
5
|
+
export function registerCiInitTool(server: McpServer) {
|
|
6
|
+
server.registerTool("ci_init", {
|
|
7
|
+
description:
|
|
8
|
+
"Generate a CI/CD workflow file for running API tests automatically on push, PR, and schedule. " +
|
|
9
|
+
"Supports GitHub Actions and GitLab CI. Auto-detects platform from project structure " +
|
|
10
|
+
"(.github/ → GitHub, .gitlab-ci.yml → GitLab). " +
|
|
11
|
+
"Use after tests are generated and passing. After generating the workflow, help the user commit and push to activate CI.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
platform: z.optional(z.enum(["github", "gitlab"]))
|
|
14
|
+
.describe("CI platform. If omitted, auto-detects from project structure (defaults to GitHub)"),
|
|
15
|
+
force: z.optional(z.boolean())
|
|
16
|
+
.describe("Overwrite existing CI config (default: false)"),
|
|
17
|
+
dir: z.optional(z.string())
|
|
18
|
+
.describe("Project root directory where CI config will be created (default: current working directory)"),
|
|
19
|
+
},
|
|
20
|
+
}, async ({ platform, force, dir }) => {
|
|
21
|
+
// Capture stdout to return as result
|
|
22
|
+
const logs: string[] = [];
|
|
23
|
+
const origWrite = process.stdout.write;
|
|
24
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
25
|
+
logs.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
26
|
+
return true;
|
|
27
|
+
}) as typeof process.stdout.write;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const code = await ciInitCommand({
|
|
31
|
+
platform,
|
|
32
|
+
force: force ?? false,
|
|
33
|
+
dir,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.stdout.write = origWrite;
|
|
37
|
+
|
|
38
|
+
const output = logs.join("").trim();
|
|
39
|
+
if (code !== 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: output || "ci init failed", exitCode: code }, null, 2) }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: JSON.stringify({ message: output, exitCode: 0 }, null, 2) }],
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
process.stdout.write = origWrite;
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints } from "../../core/generator/index.ts";
|
|
4
|
+
|
|
5
|
+
export function registerCoverageAnalysisTool(server: McpServer) {
|
|
6
|
+
server.registerTool("coverage_analysis", {
|
|
7
|
+
description: "Compare an OpenAPI spec against existing test files to find untested endpoints. " +
|
|
8
|
+
"Use to identify gaps and prioritize which endpoints to generate tests for next.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
|
|
11
|
+
testsDir: z.string().describe("Path to directory with test YAML files"),
|
|
12
|
+
},
|
|
13
|
+
}, async ({ specPath, testsDir }) => {
|
|
14
|
+
try {
|
|
15
|
+
const doc = await readOpenApiSpec(specPath);
|
|
16
|
+
const allEndpoints = extractEndpoints(doc);
|
|
17
|
+
|
|
18
|
+
if (allEndpoints.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: "No endpoints found in the spec" }, null, 2) }],
|
|
21
|
+
isError: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const covered = await scanCoveredEndpoints(testsDir);
|
|
26
|
+
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
27
|
+
const coveredCount = allEndpoints.length - uncovered.length;
|
|
28
|
+
const percentage = Math.round((coveredCount / allEndpoints.length) * 100);
|
|
29
|
+
|
|
30
|
+
const result = {
|
|
31
|
+
totalEndpoints: allEndpoints.length,
|
|
32
|
+
covered: coveredCount,
|
|
33
|
+
uncovered: uncovered.length,
|
|
34
|
+
percentage,
|
|
35
|
+
uncoveredEndpoints: uncovered.map(ep => ({
|
|
36
|
+
method: ep.method,
|
|
37
|
+
path: ep.path,
|
|
38
|
+
summary: ep.summary,
|
|
39
|
+
tags: ep.tags,
|
|
40
|
+
})),
|
|
41
|
+
coveredEndpoints: covered.map(ep => ({
|
|
42
|
+
method: ep.method,
|
|
43
|
+
path: ep.path,
|
|
44
|
+
file: ep.file,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { compressSchema, formatParam } from "../../core/generator/schema-utils.ts";
|
|
5
|
+
|
|
6
|
+
export function registerExploreApiTool(server: McpServer) {
|
|
7
|
+
server.registerTool("explore_api", {
|
|
8
|
+
description: "Explore an OpenAPI spec — list endpoints, servers, and security schemes. " +
|
|
9
|
+
"Use with includeSchemas=true when generating tests to get full request/response body schemas.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
|
|
12
|
+
tag: z.optional(z.string()).describe("Filter endpoints by tag"),
|
|
13
|
+
includeSchemas: z.optional(z.boolean()).describe("Include request/response body schemas and parameter types (default: false)"),
|
|
14
|
+
},
|
|
15
|
+
}, async ({ specPath, tag, includeSchemas }) => {
|
|
16
|
+
try {
|
|
17
|
+
const doc = await readOpenApiSpec(specPath);
|
|
18
|
+
const allEndpoints = extractEndpoints(doc);
|
|
19
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
20
|
+
const servers = ((doc as any).servers ?? []) as Array<{ url: string; description?: string }>;
|
|
21
|
+
|
|
22
|
+
const endpoints = tag
|
|
23
|
+
? allEndpoints.filter(ep => ep.tags.includes(tag))
|
|
24
|
+
: allEndpoints;
|
|
25
|
+
|
|
26
|
+
const result = {
|
|
27
|
+
title: (doc as any).info?.title,
|
|
28
|
+
version: (doc as any).info?.version,
|
|
29
|
+
servers: servers.map(s => ({ url: s.url, description: s.description })),
|
|
30
|
+
securitySchemes: securitySchemes.map(s => ({
|
|
31
|
+
name: s.name,
|
|
32
|
+
type: s.type,
|
|
33
|
+
...(s.scheme ? { scheme: s.scheme } : {}),
|
|
34
|
+
...(s.in ? { in: s.in, keyName: s.apiKeyName } : {}),
|
|
35
|
+
})),
|
|
36
|
+
totalEndpoints: allEndpoints.length,
|
|
37
|
+
...(tag ? { filteredByTag: tag, matchingEndpoints: endpoints.length } : {}),
|
|
38
|
+
endpoints: endpoints.map(ep => {
|
|
39
|
+
const base: Record<string, unknown> = {
|
|
40
|
+
method: ep.method,
|
|
41
|
+
path: ep.path,
|
|
42
|
+
summary: ep.summary,
|
|
43
|
+
tags: ep.tags,
|
|
44
|
+
parameters: ep.parameters.map(p => ({
|
|
45
|
+
name: p.name,
|
|
46
|
+
in: p.in,
|
|
47
|
+
required: p.required ?? false,
|
|
48
|
+
...(includeSchemas ? { type: formatParam(p).split(": ")[1] } : {}),
|
|
49
|
+
})),
|
|
50
|
+
hasRequestBody: !!ep.requestBodySchema,
|
|
51
|
+
responses: ep.responses.map(r => ({
|
|
52
|
+
statusCode: r.statusCode,
|
|
53
|
+
description: r.description,
|
|
54
|
+
...(includeSchemas && r.schema ? { schema: compressSchema(r.schema) } : {}),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (includeSchemas) {
|
|
59
|
+
if (ep.requestBodySchema) {
|
|
60
|
+
base.requestBodySchema = compressSchema(ep.requestBodySchema);
|
|
61
|
+
}
|
|
62
|
+
if (ep.requestBodyContentType) {
|
|
63
|
+
base.requestBodyContentType = ep.requestBodyContentType;
|
|
64
|
+
}
|
|
65
|
+
if (ep.security.length > 0) {
|
|
66
|
+
base.security = ep.security;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return base;
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import {
|
|
4
|
+
readOpenApiSpec,
|
|
5
|
+
extractEndpoints,
|
|
6
|
+
extractSecuritySchemes,
|
|
7
|
+
scanCoveredEndpoints,
|
|
8
|
+
filterUncoveredEndpoints,
|
|
9
|
+
} from "../../core/generator/index.ts";
|
|
10
|
+
import { compressEndpointsWithSchemas, buildGenerationGuide } from "./generate-tests-guide.ts";
|
|
11
|
+
|
|
12
|
+
export function registerGenerateMissingTestsTool(server: McpServer) {
|
|
13
|
+
server.registerTool("generate_missing_tests", {
|
|
14
|
+
description: "Analyze test coverage and generate a test guide for only the uncovered endpoints. " +
|
|
15
|
+
"Combines coverage_analysis + generate_tests_guide — returns a focused guide for missing tests. " +
|
|
16
|
+
"Use this for incremental test generation to avoid duplicating existing tests.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
specPath: z.string().describe("Path or URL to OpenAPI spec file"),
|
|
19
|
+
testsDir: z.string().describe("Path to directory with existing test YAML files"),
|
|
20
|
+
outputDir: z.optional(z.string()).describe("Directory for saving new test files (default: same as testsDir)"),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ specPath, testsDir, outputDir }) => {
|
|
23
|
+
try {
|
|
24
|
+
const doc = await readOpenApiSpec(specPath);
|
|
25
|
+
const allEndpoints = extractEndpoints(doc);
|
|
26
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
27
|
+
const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
|
|
28
|
+
const title = (doc as any).info?.title as string | undefined;
|
|
29
|
+
|
|
30
|
+
if (allEndpoints.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: "No endpoints found in the spec" }, null, 2) }],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const covered = await scanCoveredEndpoints(testsDir);
|
|
38
|
+
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
39
|
+
const coveredCount = allEndpoints.length - uncovered.length;
|
|
40
|
+
const percentage = Math.round((coveredCount / allEndpoints.length) * 100);
|
|
41
|
+
|
|
42
|
+
if (uncovered.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: JSON.stringify({
|
|
47
|
+
fullyCovered: true,
|
|
48
|
+
percentage: 100,
|
|
49
|
+
totalEndpoints: allEndpoints.length,
|
|
50
|
+
covered: coveredCount,
|
|
51
|
+
}, null, 2),
|
|
52
|
+
}],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build guide for uncovered endpoints only
|
|
57
|
+
const apiContext = compressEndpointsWithSchemas(uncovered, securitySchemes);
|
|
58
|
+
const coverageHeader = `## Coverage: ${coveredCount}/${allEndpoints.length} endpoints covered (${percentage}%). Generating tests for ${uncovered.length} uncovered endpoints:`;
|
|
59
|
+
|
|
60
|
+
const guide = buildGenerationGuide({
|
|
61
|
+
title: title ?? "API",
|
|
62
|
+
baseUrl,
|
|
63
|
+
apiContext,
|
|
64
|
+
outputDir: outputDir ?? testsDir,
|
|
65
|
+
securitySchemes,
|
|
66
|
+
endpointCount: uncovered.length,
|
|
67
|
+
coverageHeader,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text" as const, text: guide }],
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|