qaeverest-mcp 1.1.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/LICENSE +21 -0
- package/README.md +236 -0
- package/package.json +46 -0
- package/src/index.js +488 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QAEverest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# QAEverest MCP Server
|
|
2
|
+
|
|
3
|
+
Generate AI-powered test cases — and run security & performance checks — from inside your IDE. This [Model Context Protocol](https://modelcontextprotocol.io) server connects AI coding agents — **Claude Code, Cursor, VS Code Copilot, Windsurf**, and any other MCP-compatible client — to the QAEverest public API.
|
|
4
|
+
|
|
5
|
+
Ask your agent things like:
|
|
6
|
+
|
|
7
|
+
> *"Generate functional test cases for the login story in `auth/README.md` and save them as a feature file."*
|
|
8
|
+
>
|
|
9
|
+
> *"Generate API tests from this `openapi.yaml`, then scaffold Playwright specs from them."*
|
|
10
|
+
>
|
|
11
|
+
> *"Generate UI automation test cases for the checkout flow."*
|
|
12
|
+
>
|
|
13
|
+
> *"Run a full security scan against https://staging.example.com."*
|
|
14
|
+
>
|
|
15
|
+
> *"Run a 30-second load test on the /search endpoint."*
|
|
16
|
+
>
|
|
17
|
+
> *"How many QAEverest credits do I have left?"*
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- **Node.js 18+**
|
|
22
|
+
- A **QAEverest API key** (`qae_...`) — issued from **Admin → API Management** or by your account manager.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The server is configured entirely through environment variables:
|
|
27
|
+
|
|
28
|
+
| Variable | Required | Description |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `QAEVEREST_API_KEY` | yes | Your QAEverest API key (`qae_...`) |
|
|
31
|
+
| `QAEVEREST_API_URL` | no | Backend base URL. Defaults to `https://api.qaeverest.ai` |
|
|
32
|
+
|
|
33
|
+
The server exchanges your API key for a short-lived access token automatically and refreshes it when it expires — you never handle tokens yourself.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
### Claude Code
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
claude mcp add qaeverest -e QAEVEREST_API_KEY=qae_your_key_here -- npx qaeverest-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or add to your project's `.mcp.json` (checked in, so the whole team gets it — keep the key in an env var):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"qaeverest": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["qaeverest-mcp"],
|
|
51
|
+
"env": {
|
|
52
|
+
"QAEVEREST_API_KEY": "${QAEVEREST_API_KEY}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Cursor
|
|
60
|
+
|
|
61
|
+
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"qaeverest": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["qaeverest-mcp"],
|
|
69
|
+
"env": {
|
|
70
|
+
"QAEVEREST_API_KEY": "qae_your_key_here"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### VS Code (GitHub Copilot agent mode)
|
|
78
|
+
|
|
79
|
+
Add to `.vscode/mcp.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"servers": {
|
|
84
|
+
"qaeverest": {
|
|
85
|
+
"type": "stdio",
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["qaeverest-mcp"],
|
|
88
|
+
"env": {
|
|
89
|
+
"QAEVEREST_API_KEY": "${input:qaeverest-api-key}"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"inputs": [
|
|
94
|
+
{
|
|
95
|
+
"id": "qaeverest-api-key",
|
|
96
|
+
"type": "promptString",
|
|
97
|
+
"description": "QAEverest API key",
|
|
98
|
+
"password": true
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Windsurf
|
|
105
|
+
|
|
106
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"qaeverest": {
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": ["qaeverest-mcp"],
|
|
114
|
+
"env": {
|
|
115
|
+
"QAEVEREST_API_KEY": "qae_your_key_here"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Running from a local checkout (before npm publish)
|
|
123
|
+
|
|
124
|
+
Replace `"command": "npx", "args": ["qaeverest-mcp"]` with:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
"command": "node",
|
|
128
|
+
"args": ["C:/path/to/CreateMyTestcases/mcp-server/src/index.js"]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
(run `npm install` inside `mcp-server/` first).
|
|
132
|
+
|
|
133
|
+
## Tools
|
|
134
|
+
|
|
135
|
+
The server exposes these tools. Each generation/scan call is billed to your account at an admin-configured per-service credit cost; whether a tool works depends on which services are **enabled** for your API key (check with `get_usage`). You don't call these directly — describe what you want and the agent picks the tool.
|
|
136
|
+
|
|
137
|
+
### `generate_testcases` — functional / general (sync)
|
|
138
|
+
|
|
139
|
+
Generate test cases from a user story or feature description. Returns Gherkin-style scenarios (Given/When/Then) with tags, priorities, and test data. Generation typically takes 1–3 minutes.
|
|
140
|
+
|
|
141
|
+
| Parameter | Type | Required | Description |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `user_story` | string | yes | The story / requirement / feature description |
|
|
144
|
+
| `test_type` | enum | no | `Functional` (default), `System`, `Performance`, `Security`, `API` |
|
|
145
|
+
| `tc_type` | enum | no | `manual` (default) or `automated` |
|
|
146
|
+
| `domain_name` | string | no | Business domain context, e.g. `Banking` |
|
|
147
|
+
| `module_name` | string | no | Feature area, e.g. `Login` |
|
|
148
|
+
| `additional` | string | no | Extra instructions for the AI |
|
|
149
|
+
|
|
150
|
+
### `generate_api_tests` — API automation (sync)
|
|
151
|
+
|
|
152
|
+
Generate automation-ready API test cases from a description **or an OpenAPI/Swagger spec or Postman collection pasted as text** (positive, negative, boundary, auth).
|
|
153
|
+
|
|
154
|
+
| Parameter | Type | Required | Description |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| `spec` | string | yes | API description, **or** an OpenAPI/Swagger spec or Postman collection as text |
|
|
157
|
+
| `tc_type` | enum | no | `manual` or `automated` (default) |
|
|
158
|
+
| `domain_name`, `module_name`, `additional` | string | no | As above |
|
|
159
|
+
|
|
160
|
+
### `generate_ui_tests` — UI automation (async)
|
|
161
|
+
|
|
162
|
+
Generate end-to-end UI automation test cases (navigate / click / type / assert steps). Runs as a background job and may take a few minutes. **Generates test cases only — it does not drive a live browser.**
|
|
163
|
+
|
|
164
|
+
| Parameter | Type | Required | Description |
|
|
165
|
+
|---|---|---|---|
|
|
166
|
+
| `story` | string | yes | The UI scenario / user story |
|
|
167
|
+
| `domain_name`, `module_name`, `additional` | string | no | As above |
|
|
168
|
+
|
|
169
|
+
### `generate_mobile_tests` — mobile automation (async)
|
|
170
|
+
|
|
171
|
+
Same as `generate_ui_tests`, but for mobile UI automation (gestures, device states, platform-specific behaviours). Generates test cases only — it does not drive a live device.
|
|
172
|
+
|
|
173
|
+
| Parameter | Type | Required | Description |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `story` | string | yes | The mobile scenario / user story |
|
|
176
|
+
| `domain_name`, `module_name`, `additional` | string | no | As above |
|
|
177
|
+
|
|
178
|
+
### `security_scan` — security (sync)
|
|
179
|
+
|
|
180
|
+
Scan a URL for security-header, SSL/TLS, and common-vulnerability issues; returns risk-rated findings.
|
|
181
|
+
|
|
182
|
+
| Parameter | Type | Required | Description |
|
|
183
|
+
|---|---|---|---|
|
|
184
|
+
| `url` | string | yes | Full URL including `http://` or `https://` |
|
|
185
|
+
| `scanType` | enum | no | `headers`, `ssl`, `vulnerability`, or `full` (default) |
|
|
186
|
+
| `method` | enum | no | HTTP method to probe with (default `GET`) |
|
|
187
|
+
| `headers` | object | no | Optional request headers (key/value) |
|
|
188
|
+
|
|
189
|
+
### `performance_test` — performance (sync)
|
|
190
|
+
|
|
191
|
+
Run a load / stress / spike / soak test and return latency, throughput, and error-rate metrics. API-tier caps keep a single call bounded (~1 min); heavier runs belong in the in-app Performance module.
|
|
192
|
+
|
|
193
|
+
| Parameter | Type | Required | Description |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| `url` | string | yes | Full URL including `http://` or `https://` |
|
|
196
|
+
| `testMode` | enum | no | `load` (default), `stress`, `spike`, `soak` |
|
|
197
|
+
| `method` | enum | no | HTTP method (default `GET`) |
|
|
198
|
+
| `headers` | object | no | Optional request headers (key/value) |
|
|
199
|
+
| `body` | string | no | Optional request body (POST/PUT) |
|
|
200
|
+
| `virtualUsers` | number | no | Concurrent virtual users (load/soak); server caps apply |
|
|
201
|
+
| `duration` | number | no | Seconds; server caps apply (load ≤ 60s, soak ≤ 90s) |
|
|
202
|
+
| `rampUp` | number | no | Ramp-up seconds (load mode) |
|
|
203
|
+
|
|
204
|
+
### `get_usage`
|
|
205
|
+
|
|
206
|
+
No parameters. Returns credits used/remaining, requests this month, and enabled services for your account.
|
|
207
|
+
|
|
208
|
+
## How it works
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
IDE agent (Claude Code / Cursor / Copilot / Windsurf)
|
|
212
|
+
│ MCP over stdio
|
|
213
|
+
▼
|
|
214
|
+
qaeverest-mcp
|
|
215
|
+
│ POST /api/v1/auth/token (API key → 8h JWT, cached)
|
|
216
|
+
│ POST /api/v1/generate-testcases ─┐
|
|
217
|
+
│ POST /api/v1/api-tests/generate │ sync — returns inline
|
|
218
|
+
│ POST /api/v1/security-scan │
|
|
219
|
+
│ POST /api/v1/performance-test ─┘
|
|
220
|
+
│ POST /api/v1/ui-tests → poll GET /api/v1/ui-tests/{jobId} ─┐ async —
|
|
221
|
+
│ POST /api/v1/mobile-tests → poll GET /api/v1/mobile-tests/{jobId} ─┘ server polls for you
|
|
222
|
+
│ GET /api/v1/usage
|
|
223
|
+
▼
|
|
224
|
+
QAEverest backend ──► AI engine / scanners ──► results
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
For the async UI/mobile tools the server submits the job and polls the status URL for you (up to 5 minutes), so from the agent's point of view they behave like any other tool call.
|
|
228
|
+
|
|
229
|
+
## Troubleshooting
|
|
230
|
+
|
|
231
|
+
- **"QAEVEREST_API_KEY is not set"** — the env var didn't reach the server; check the `env` block in your MCP config.
|
|
232
|
+
- **"Invalid or revoked API key"** — the key was revoked or regenerated in Admin → API Management; get a fresh one.
|
|
233
|
+
- **"Insufficient credits" (402)** — your plan's credit allowance is exhausted.
|
|
234
|
+
- **"Monthly request limit reached" (429)** — resets at the next billing cycle.
|
|
235
|
+
- **Timeouts** — generation is capped at 5 minutes client-side; very long stories may need trimming.
|
|
236
|
+
- The server logs diagnostics to **stderr** (visible in your IDE's MCP logs, e.g. `claude mcp list` / Cursor's MCP output panel).
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qaeverest-mcp",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server for QAEverest — generate AI-powered functional, API, UI & mobile test cases, plus security and performance checks, from inside Claude Code, Cursor, VS Code Copilot, or any MCP-compatible IDE.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qaeverest-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"test": "node smoke-test.mjs"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"modelcontextprotocol",
|
|
25
|
+
"qaeverest",
|
|
26
|
+
"testing",
|
|
27
|
+
"test-cases",
|
|
28
|
+
"qa",
|
|
29
|
+
"ai"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": "QAEverest",
|
|
33
|
+
"homepage": "https://qaeverest.ai",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/pooja10404/CreateMyTestcases.git",
|
|
37
|
+
"directory": "mcp-server"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/pooja10404/CreateMyTestcases/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
44
|
+
"zod": "^3.23.8"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* QAEverest MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Exposes QAEverest's public v1 API as MCP tools over stdio so AI coding
|
|
6
|
+
* agents (Claude Code, Cursor, VS Code Copilot, Windsurf, ...) can generate
|
|
7
|
+
* test cases — and run security/performance checks — without leaving the IDE.
|
|
8
|
+
*
|
|
9
|
+
* Configuration (environment variables):
|
|
10
|
+
* QAEVEREST_API_KEY (required) — your QAEverest API key (qae_...)
|
|
11
|
+
* QAEVEREST_API_URL (optional) — backend base URL, default https://api.qaeverest.ai
|
|
12
|
+
*
|
|
13
|
+
* NOTE: stdout carries the JSON-RPC protocol — all diagnostics go to stderr.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
const API_KEY = process.env.QAEVEREST_API_KEY;
|
|
21
|
+
const BASE_URL = (process.env.QAEVEREST_API_URL || "https://api.qaeverest.ai").replace(/\/+$/, "");
|
|
22
|
+
|
|
23
|
+
if (!API_KEY) {
|
|
24
|
+
console.error(
|
|
25
|
+
"[qaeverest-mcp] QAEVEREST_API_KEY is not set.\n" +
|
|
26
|
+
"Get an API key from your QAEverest account manager (Admin → API Management),\n" +
|
|
27
|
+
"then add it to your MCP config, e.g.:\n" +
|
|
28
|
+
' claude mcp add qaeverest -e QAEVEREST_API_KEY=qae_xxx -- npx qaeverest-mcp'
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
34
|
+
|
|
35
|
+
// ── Token management ──────────────────────────────────────────────────────────
|
|
36
|
+
// The public API uses short-lived JWTs (8h) exchanged from the API key.
|
|
37
|
+
// Cache the token and refresh a few minutes before expiry or on a 401.
|
|
38
|
+
|
|
39
|
+
const TOKEN_TTL_MS = 8 * 60 * 60 * 1000;
|
|
40
|
+
const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
41
|
+
|
|
42
|
+
let cachedToken = null;
|
|
43
|
+
let tokenExpiresAt = 0;
|
|
44
|
+
|
|
45
|
+
async function getToken(forceRefresh = false) {
|
|
46
|
+
if (!forceRefresh && cachedToken && Date.now() < tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS) {
|
|
47
|
+
return cachedToken;
|
|
48
|
+
}
|
|
49
|
+
const res = await fetch(`${BASE_URL}/api/v1/auth/token`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ apiKey: API_KEY }),
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json().catch(() => ({}));
|
|
55
|
+
if (!res.ok || !data.token) {
|
|
56
|
+
throw new Error(data.error || `Authentication failed (HTTP ${res.status}). Check QAEVEREST_API_KEY.`);
|
|
57
|
+
}
|
|
58
|
+
cachedToken = data.token;
|
|
59
|
+
tokenExpiresAt = Date.now() + TOKEN_TTL_MS;
|
|
60
|
+
return cachedToken;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function apiFetch(path, { method = "GET", body, timeoutMs = 5 * 60 * 1000 } = {}, retryOn401 = true) {
|
|
64
|
+
const token = await getToken();
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
69
|
+
method,
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
Authorization: `Bearer ${token}`,
|
|
73
|
+
},
|
|
74
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
if (res.status === 401 && retryOn401) {
|
|
78
|
+
await getToken(true);
|
|
79
|
+
return apiFetch(path, { method, body, timeoutMs }, false);
|
|
80
|
+
}
|
|
81
|
+
const data = await res.json().catch(() => ({}));
|
|
82
|
+
return { status: res.status, ok: res.ok, data };
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Result helpers ────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const errorResult = (message) => ({
|
|
91
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
92
|
+
isError: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const textResult = (text) => ({ content: [{ type: "text", text }] });
|
|
96
|
+
|
|
97
|
+
// Map a non-2xx response from a metered endpoint to a friendly tool error.
|
|
98
|
+
function mapHttpError(status, data) {
|
|
99
|
+
if (status === 402) return errorResult(`${data.error || "Insufficient credits."} Top up your QAEverest plan to continue.`);
|
|
100
|
+
if (status === 403) return errorResult(data.error || "This service isn't enabled for your account. Ask your QAEverest admin to enable it.");
|
|
101
|
+
if (status === 429) return errorResult(data.error || "Monthly request limit reached.");
|
|
102
|
+
return errorResult(data.error || `QAEverest API returned HTTP ${status}.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function creditFooter(result) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (result.creditsUsed !== undefined) parts.push(`Credits used: ${result.creditsUsed}`);
|
|
108
|
+
if (result.creditsRemaining !== undefined) parts.push(`Credits remaining: ${result.creditsRemaining}`);
|
|
109
|
+
return parts.length ? ["---", parts.join(" · ")] : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Output formatting ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function formatTestcases(result, heading = "Generated Test Cases") {
|
|
115
|
+
const list = result.testcases || [];
|
|
116
|
+
const lines = [`# ${heading} (${result.count ?? list.length})`, ""];
|
|
117
|
+
for (const tc of list) {
|
|
118
|
+
const tags = (tc.tags || []).join(" ");
|
|
119
|
+
lines.push(`## ${tc.id ? `[${tc.id}] ` : ""}${tc.title || "Untitled scenario"}`);
|
|
120
|
+
const meta = [
|
|
121
|
+
tc.category && `Category: ${tc.category}`,
|
|
122
|
+
tc.priority && `Priority: ${tc.priority}`,
|
|
123
|
+
tags && `Tags: ${tags}`,
|
|
124
|
+
].filter(Boolean);
|
|
125
|
+
if (meta.length) lines.push(`*${meta.join(" · ")}*`);
|
|
126
|
+
lines.push("");
|
|
127
|
+
if (tc.steps?.length) {
|
|
128
|
+
lines.push("```gherkin");
|
|
129
|
+
for (const step of tc.steps) lines.push(step);
|
|
130
|
+
lines.push("```");
|
|
131
|
+
}
|
|
132
|
+
if (tc.test_data) {
|
|
133
|
+
lines.push("**Test data:**", "```json", JSON.stringify(tc.test_data, null, 2), "```");
|
|
134
|
+
}
|
|
135
|
+
if (tc.examples) {
|
|
136
|
+
lines.push("**Examples:**", "```json", JSON.stringify(tc.examples, null, 2), "```");
|
|
137
|
+
}
|
|
138
|
+
lines.push("");
|
|
139
|
+
}
|
|
140
|
+
lines.push(...creditFooter(result));
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatSecurity(r) {
|
|
145
|
+
const lines = [`# Security Scan — ${r.url}`, ""];
|
|
146
|
+
lines.push(`**Scan type:** ${r.scanType} · **Overall risk:** ${String(r.overallRisk || "unknown").toUpperCase()}`);
|
|
147
|
+
const ic = r.issueCount || {};
|
|
148
|
+
lines.push(`**Issues:** ${["critical", "high", "medium", "low", "info"].map((k) => `${k} ${ic[k] || 0}`).join(" · ")}`, "");
|
|
149
|
+
|
|
150
|
+
const res = r.results || {};
|
|
151
|
+
const findingLine = (f) => {
|
|
152
|
+
const label = f.header || f.title || f.name || f.check || "Finding";
|
|
153
|
+
const detail = f.message || f.description || "";
|
|
154
|
+
return `- **[${String(f.risk || "info").toUpperCase()}]** ${label}${detail ? ` — ${detail}` : ""}`;
|
|
155
|
+
};
|
|
156
|
+
const section = (title, items) => {
|
|
157
|
+
const flagged = (items || []).filter((f) => f && f.risk && f.risk !== "pass");
|
|
158
|
+
if (flagged.length) { lines.push(`## ${title}`, ...flagged.map(findingLine), ""); }
|
|
159
|
+
};
|
|
160
|
+
section("Security header findings", res.headerFindings);
|
|
161
|
+
section("Vulnerability findings", res.vulnFindings);
|
|
162
|
+
if (res.sslResult) {
|
|
163
|
+
const ssl = res.sslResult;
|
|
164
|
+
lines.push("## SSL / TLS", `- HTTPS: ${ssl.hasSSL === false ? "not available" : "available"}`);
|
|
165
|
+
if (Array.isArray(ssl.issues) && ssl.issues.length) ssl.issues.forEach((i) => lines.push(`- ${i}`));
|
|
166
|
+
lines.push("");
|
|
167
|
+
}
|
|
168
|
+
if (res.httpStatusCode != null) lines.push(`HTTP status: ${res.httpStatusCode}`);
|
|
169
|
+
lines.push(...creditFooter(r));
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatPerformance(r) {
|
|
174
|
+
const lines = [`# Performance Test — ${r.url}`, "", `**Mode:** ${r.testMode}`, "", "## Metrics"];
|
|
175
|
+
const m = r.metrics || {};
|
|
176
|
+
const rows = [
|
|
177
|
+
["Total requests", m.totalRequests],
|
|
178
|
+
["Success / failure", `${m.successCount} / ${m.failureCount}`],
|
|
179
|
+
["Error rate", `${m.errorRate}%`],
|
|
180
|
+
["Avg response", `${m.avgResponseMs} ms`],
|
|
181
|
+
["p95 / p99", `${m.p95ResponseMs} / ${m.p99ResponseMs} ms`],
|
|
182
|
+
["Min / max", `${m.minResponseMs} / ${m.maxResponseMs} ms`],
|
|
183
|
+
["Throughput", `${m.throughput} req/s`],
|
|
184
|
+
];
|
|
185
|
+
for (const [k, v] of rows) if (v !== undefined) lines.push(`- **${k}:** ${v}`);
|
|
186
|
+
for (const key of ["stressResult", "spikeResult", "soakSegments"]) {
|
|
187
|
+
if (r[key]) lines.push("", `## ${key}`, "```json", JSON.stringify(r[key], null, 2), "```");
|
|
188
|
+
}
|
|
189
|
+
lines.push("", ...creditFooter(r));
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Async (job-based) generation: submit, then poll until done ─────────────────
|
|
194
|
+
|
|
195
|
+
async function pollJob(path, { intervalMs = 4000, timeoutMs = 5 * 60 * 1000 } = {}) {
|
|
196
|
+
const deadline = Date.now() + timeoutMs;
|
|
197
|
+
while (Date.now() < deadline) {
|
|
198
|
+
await sleep(intervalMs);
|
|
199
|
+
const { ok, data } = await apiFetch(path, { method: "GET", timeoutMs: 30 * 1000 });
|
|
200
|
+
if (ok && (data.status === "completed" || data.status === "failed")) return data;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function runAsyncGeneration({ submitPath, body, heading }) {
|
|
206
|
+
try {
|
|
207
|
+
const submit = await apiFetch(submitPath, { method: "POST", body, timeoutMs: 60 * 1000 });
|
|
208
|
+
if (!submit.ok) return mapHttpError(submit.status, submit.data);
|
|
209
|
+
|
|
210
|
+
const jobId = submit.data.jobId;
|
|
211
|
+
if (!jobId) return errorResult("Job was accepted but no job id was returned.");
|
|
212
|
+
const statusPath = submit.data.statusUrl || `${submitPath}/${jobId}`;
|
|
213
|
+
|
|
214
|
+
const job = await pollJob(statusPath);
|
|
215
|
+
if (!job) return errorResult("Generation is still running after 5 minutes. Try again later or simplify the story.");
|
|
216
|
+
if (job.status === "failed") return errorResult(job.error || "Generation failed.");
|
|
217
|
+
|
|
218
|
+
return textResult(formatTestcases({ ...(job.result || {}), creditsUsed: job.creditsUsed }, heading));
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err.name === "AbortError") return errorResult("Request timed out. Please try again.");
|
|
221
|
+
return errorResult(err.message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Shared input fields for AI generation tools.
|
|
226
|
+
const genFields = {
|
|
227
|
+
domain_name: z.string().optional().describe('Business domain for context, e.g. "Banking", "Healthcare", "E-commerce".'),
|
|
228
|
+
module_name: z.string().optional().describe('Application module / feature area, e.g. "Login", "Checkout".'),
|
|
229
|
+
additional: z.string().optional().describe("Extra instructions for the AI, e.g. focus areas, tools, or constraints."),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── MCP server + tools ────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const server = new McpServer({ name: "qaeverest", version: "1.1.0" });
|
|
235
|
+
|
|
236
|
+
// 1. Functional / general test-case generation (sync).
|
|
237
|
+
server.registerTool(
|
|
238
|
+
"generate_testcases",
|
|
239
|
+
{
|
|
240
|
+
title: "Generate Test Cases",
|
|
241
|
+
description:
|
|
242
|
+
"Generate AI-powered QA test cases from a user story or feature description using QAEverest. " +
|
|
243
|
+
"Returns structured test scenarios in Gherkin format (Given/When/Then) covering positive, negative " +
|
|
244
|
+
"and edge cases. Consumes 1 QAEverest credit per call. Generation can take 1-3 minutes.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
user_story: z
|
|
247
|
+
.string()
|
|
248
|
+
.min(10)
|
|
249
|
+
.describe(
|
|
250
|
+
"The user story, requirement, or feature description to generate test cases from. " +
|
|
251
|
+
"More detail produces better coverage (acceptance criteria, validation rules, edge conditions)."
|
|
252
|
+
),
|
|
253
|
+
test_type: z
|
|
254
|
+
.enum(["Functional", "System", "Performance", "Security", "API"])
|
|
255
|
+
.default("Functional")
|
|
256
|
+
.describe("The kind of testing to generate test cases for."),
|
|
257
|
+
tc_type: z
|
|
258
|
+
.enum(["manual", "automated"])
|
|
259
|
+
.default("manual")
|
|
260
|
+
.describe("Whether test cases are written for manual execution or as automation-ready scenarios."),
|
|
261
|
+
...genFields,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
async ({ user_story, test_type, domain_name, module_name, tc_type, additional }) => {
|
|
265
|
+
try {
|
|
266
|
+
const { status, ok, data } = await apiFetch("/api/v1/generate-testcases", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: { story: user_story, test_type, domain_name, module_name, tc_type, additional },
|
|
269
|
+
});
|
|
270
|
+
if (!ok) return mapHttpError(status, data);
|
|
271
|
+
return textResult(formatTestcases(data));
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (err.name === "AbortError") return errorResult("Generation timed out after 5 minutes. Please try again.");
|
|
274
|
+
return errorResult(err.message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// 2. API test generation (sync) — accepts a story OR an OpenAPI/Postman spec.
|
|
280
|
+
server.registerTool(
|
|
281
|
+
"generate_api_tests",
|
|
282
|
+
{
|
|
283
|
+
title: "Generate API Tests",
|
|
284
|
+
description:
|
|
285
|
+
"Generate AI-powered API test cases with QAEverest. Accepts either a plain description of the API " +
|
|
286
|
+
"to test, OR an OpenAPI/Swagger spec or Postman collection pasted as text. Returns automation-ready " +
|
|
287
|
+
"API scenarios (positive, negative, boundary, auth). Consumes QAEverest credits (apiAutomation service). " +
|
|
288
|
+
"Generation can take 1-3 minutes.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
spec: z
|
|
291
|
+
.string()
|
|
292
|
+
.min(10)
|
|
293
|
+
.describe(
|
|
294
|
+
"Either a description of the API/endpoint to test, OR an OpenAPI/Swagger spec or Postman " +
|
|
295
|
+
"collection pasted as text."
|
|
296
|
+
),
|
|
297
|
+
tc_type: z
|
|
298
|
+
.enum(["manual", "automated"])
|
|
299
|
+
.default("automated")
|
|
300
|
+
.describe("manual, or automation-ready scenarios (default)."),
|
|
301
|
+
...genFields,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
async ({ spec, tc_type, domain_name, module_name, additional }) => {
|
|
305
|
+
try {
|
|
306
|
+
const { status, ok, data } = await apiFetch("/api/v1/api-tests/generate", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: { spec, tc_type, domain_name, module_name, additional },
|
|
309
|
+
});
|
|
310
|
+
if (!ok) return mapHttpError(status, data);
|
|
311
|
+
return textResult(formatTestcases(data, "Generated API Test Cases"));
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err.name === "AbortError") return errorResult("Generation timed out after 5 minutes. Please try again.");
|
|
314
|
+
return errorResult(err.message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// 3. UI automation test generation (async job).
|
|
320
|
+
server.registerTool(
|
|
321
|
+
"generate_ui_tests",
|
|
322
|
+
{
|
|
323
|
+
title: "Generate UI Automation Tests",
|
|
324
|
+
description:
|
|
325
|
+
"Generate AI-powered end-to-end UI automation test cases (navigate / click / type / assert steps) from a " +
|
|
326
|
+
"scenario or user story, using QAEverest. This GENERATES the test cases — it does not drive a live browser. " +
|
|
327
|
+
"Runs as a background job and may take a few minutes. Consumes QAEverest credits (uiAutomation service).",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
story: z
|
|
330
|
+
.string()
|
|
331
|
+
.min(10)
|
|
332
|
+
.describe("The UI scenario or user story to generate end-to-end UI automation test cases for."),
|
|
333
|
+
...genFields,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
async ({ story, domain_name, module_name, additional }) =>
|
|
337
|
+
runAsyncGeneration({
|
|
338
|
+
submitPath: "/api/v1/ui-tests",
|
|
339
|
+
body: { story, domain_name, module_name, additional },
|
|
340
|
+
heading: "Generated UI Automation Test Cases",
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// 4. Mobile automation test generation (async job).
|
|
345
|
+
server.registerTool(
|
|
346
|
+
"generate_mobile_tests",
|
|
347
|
+
{
|
|
348
|
+
title: "Generate Mobile Automation Tests",
|
|
349
|
+
description:
|
|
350
|
+
"Generate AI-powered mobile UI automation test cases (covering gestures, device states, and platform-specific " +
|
|
351
|
+
"behaviours) from a scenario or user story, using QAEverest. This GENERATES the test cases — it does not drive a " +
|
|
352
|
+
"live device. Runs as a background job and may take a few minutes. Consumes QAEverest credits (mobileAutomation service).",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
story: z
|
|
355
|
+
.string()
|
|
356
|
+
.min(10)
|
|
357
|
+
.describe("The mobile scenario or user story to generate mobile automation test cases for."),
|
|
358
|
+
...genFields,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
async ({ story, domain_name, module_name, additional }) =>
|
|
362
|
+
runAsyncGeneration({
|
|
363
|
+
submitPath: "/api/v1/mobile-tests",
|
|
364
|
+
body: { story, domain_name, module_name, additional },
|
|
365
|
+
heading: "Generated Mobile Automation Test Cases",
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// 5. Security scan (sync).
|
|
370
|
+
server.registerTool(
|
|
371
|
+
"security_scan",
|
|
372
|
+
{
|
|
373
|
+
title: "Security Scan",
|
|
374
|
+
description:
|
|
375
|
+
"Run a QAEverest security scan against a URL: checks security headers, SSL/TLS configuration, and common " +
|
|
376
|
+
"vulnerabilities, and returns a risk-rated list of findings. Consumes QAEverest credits (securityAutomation service).",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
url: z.string().url().describe("The full URL to scan, including http:// or https://."),
|
|
379
|
+
scanType: z
|
|
380
|
+
.enum(["headers", "ssl", "vulnerability", "full"])
|
|
381
|
+
.default("full")
|
|
382
|
+
.describe("Which checks to run. 'full' runs all of them (default)."),
|
|
383
|
+
method: z
|
|
384
|
+
.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
|
|
385
|
+
.default("GET")
|
|
386
|
+
.describe("HTTP method to probe the URL with."),
|
|
387
|
+
headers: z
|
|
388
|
+
.record(z.string())
|
|
389
|
+
.optional()
|
|
390
|
+
.describe("Optional request headers as a key/value object (e.g. an Authorization header)."),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
async ({ url, scanType, method, headers }) => {
|
|
394
|
+
try {
|
|
395
|
+
const { status, ok, data } = await apiFetch("/api/v1/security-scan", {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: { url, scanType, method, headers },
|
|
398
|
+
});
|
|
399
|
+
if (!ok) return mapHttpError(status, data);
|
|
400
|
+
return textResult(formatSecurity(data));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err.name === "AbortError") return errorResult("Scan timed out. Please try again.");
|
|
403
|
+
return errorResult(err.message);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// 6. Performance test (sync; API-tier caps keep a single call bounded).
|
|
409
|
+
server.registerTool(
|
|
410
|
+
"performance_test",
|
|
411
|
+
{
|
|
412
|
+
title: "Performance Test",
|
|
413
|
+
description:
|
|
414
|
+
"Run a QAEverest performance test against a URL (load, stress, spike, or soak) and return latency, throughput, " +
|
|
415
|
+
"and error-rate metrics. API-tier caps keep a single call bounded (~1 minute); heavier runs belong in the in-app " +
|
|
416
|
+
"Performance module. Consumes QAEverest credits (performanceAutomation service).",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
url: z.string().url().describe("The full URL to test, including http:// or https://."),
|
|
419
|
+
testMode: z
|
|
420
|
+
.enum(["load", "stress", "spike", "soak"])
|
|
421
|
+
.default("load")
|
|
422
|
+
.describe("The performance profile to run. (stress/spike use sensible server-side defaults.)"),
|
|
423
|
+
method: z
|
|
424
|
+
.enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
|
|
425
|
+
.default("GET")
|
|
426
|
+
.describe("HTTP method for each request."),
|
|
427
|
+
headers: z.record(z.string()).optional().describe("Optional request headers as a key/value object."),
|
|
428
|
+
body: z.string().optional().describe("Optional request body (for POST/PUT)."),
|
|
429
|
+
virtualUsers: z.number().int().positive().optional().describe("Concurrent virtual users (load/soak). Server caps apply."),
|
|
430
|
+
duration: z.number().int().positive().optional().describe("Test duration in seconds. Server caps apply (load ≤ 60s, soak ≤ 90s)."),
|
|
431
|
+
rampUp: z.number().int().nonnegative().optional().describe("Ramp-up time in seconds (load mode)."),
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
async ({ url, testMode, method, headers, body, virtualUsers, duration, rampUp }) => {
|
|
435
|
+
try {
|
|
436
|
+
const { status, ok, data } = await apiFetch("/api/v1/performance-test", {
|
|
437
|
+
method: "POST",
|
|
438
|
+
body: { url, testMode, method, headers, body, virtualUsers, duration, rampUp },
|
|
439
|
+
});
|
|
440
|
+
if (!ok) return mapHttpError(status, data);
|
|
441
|
+
return textResult(formatPerformance(data));
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (err.name === "AbortError") return errorResult("Performance test timed out. Please try again.");
|
|
444
|
+
return errorResult(err.message);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// 7. Usage / credits (read-only).
|
|
450
|
+
server.registerTool(
|
|
451
|
+
"get_usage",
|
|
452
|
+
{
|
|
453
|
+
title: "Get QAEverest Usage",
|
|
454
|
+
description:
|
|
455
|
+
"Check your QAEverest API usage: credits remaining, requests this month, and which services are enabled for your account.",
|
|
456
|
+
inputSchema: {},
|
|
457
|
+
},
|
|
458
|
+
async () => {
|
|
459
|
+
try {
|
|
460
|
+
const { ok, status, data } = await apiFetch("/api/v1/usage", { timeoutMs: 30 * 1000 });
|
|
461
|
+
if (!ok) return mapHttpError(status, data);
|
|
462
|
+
|
|
463
|
+
const services = Object.entries(data.servicesEnabled || {})
|
|
464
|
+
.map(([name, enabled]) => `- ${name}: ${enabled ? "enabled" : "disabled"}`)
|
|
465
|
+
.join("\n");
|
|
466
|
+
|
|
467
|
+
const text = [
|
|
468
|
+
`# QAEverest Usage${data.company ? ` — ${data.company}` : ""}`,
|
|
469
|
+
"",
|
|
470
|
+
`Credits: ${data.creditsUsed} used of ${data.creditsLimit} (${data.creditsRemaining} remaining)`,
|
|
471
|
+
`Requests this month: ${data.requestsThisMonth}${data.requestsLimit ? ` of ${data.requestsLimit}` : ""}`,
|
|
472
|
+
"",
|
|
473
|
+
"## Services",
|
|
474
|
+
services || "(none reported)",
|
|
475
|
+
].join("\n");
|
|
476
|
+
|
|
477
|
+
return textResult(text);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
return errorResult(err.message);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
const transport = new StdioServerTransport();
|
|
487
|
+
await server.connect(transport);
|
|
488
|
+
console.error(`[qaeverest-mcp] ready — API: ${BASE_URL}`);
|