opentestmcp 0.3.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 +231 -0
- package/bin/cli.js +168 -0
- package/browser/README.md +9 -0
- package/dist/mcp-preview-panel.d.ts +30 -0
- package/dist/mcp-preview-panel.js +166 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +980 -0
- package/dist/user_auth_flow.d.ts +11 -0
- package/dist/user_auth_flow.js +251 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenTest
|
|
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,231 @@
|
|
|
1
|
+
# OpenTest MCP
|
|
2
|
+
|
|
3
|
+
AI QA engineer for your IDE. Test UI flows and API endpoints from Cursor, Claude Code, or any MCP-compatible coding agent.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **Test UI flows** in your real Chrome browser -- already logged in, no credential hassle
|
|
8
|
+
- **Test API endpoints** with assertions and variable chaining
|
|
9
|
+
- **Fullstack testing** -- run UI and API tests in parallel
|
|
10
|
+
- **Endpoint registry** -- tested endpoints are automatically shared between backend and frontend agents
|
|
11
|
+
- **`test_endpoint_by_id`** -- run a saved registry endpoint by UUID from `get_endpoints` (no manual URL assembly); optional `body` for login payloads
|
|
12
|
+
|
|
13
|
+
### Monorepo (Flow backend in this repo)
|
|
14
|
+
|
|
15
|
+
See repo root **`AGENTS.md`** for **`ot_live_`** API keys, **`POST /mcp/call-tool`**, the **golden path** (`get_endpoints` → `test_api` or `test_endpoint_by_id`), MCP **Apps** / iframe dashboard, and smoke scripts: [`scripts/smoke-call-tool.mjs`](scripts/smoke-call-tool.mjs), [`scripts/smoke-mcp-flow.mjs`](scripts/smoke-mcp-flow.mjs), [`scripts/smoke-local-endpoint-and-mcp.mjs`](scripts/smoke-local-endpoint-and-mcp.mjs).
|
|
16
|
+
|
|
17
|
+
### Local debug: prove MCP hit your endpoint
|
|
18
|
+
|
|
19
|
+
If you want to verify local end-to-end behavior (direct endpoint call + MCP `test_api` call against the same URL), run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
OPENTEST_BACKEND=http://127.0.0.1:8000 \
|
|
23
|
+
OPENTEST_API_KEY=ot_live_xxx \
|
|
24
|
+
LOGIN_EMAIL=you@example.com \
|
|
25
|
+
LOGIN_PASSWORD=your-password \
|
|
26
|
+
node opentest-mcp/scripts/smoke-local-endpoint-and-mcp.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It prints both responses and a `debug_run` id so you can match backend logs for:
|
|
30
|
+
1. `POST /mcp/call-tool` (tool dispatch)
|
|
31
|
+
2. the target endpoint hit by the MCP tool
|
|
32
|
+
|
|
33
|
+
### Real endpoints, not fake servers
|
|
34
|
+
|
|
35
|
+
OpenTest tools hit **your real HTTP APIs** (localhost or deployed). The **`preview_login_mock`** name means a **login-shaped form UI** (Postman-style), not a mocked backend: the browser runs `fetch()` to whatever **`api_base` + path** you set. In **Cursor**, the agent opens that URL in the **Agent Browser tab**, runs the same request you would in Postman, and you see **status + body** in the panel. **`test_api`** is the headless path without a browser tab.
|
|
36
|
+
|
|
37
|
+
### Deprecation: `preview_login_mock` / mock-login UI (**~2 days**)
|
|
38
|
+
|
|
39
|
+
**Removal target: 2026-04-08.** This login panel and `preview_login_mock` are in a short deprecation window so we can finish testing **hosted OpenTest MCP** (`OPENTEST_BACKEND`, `POST /mcp/call-tool`, **`test_api`** / **`get_endpoints`** without the local lite server). After that date, migrate to the hosted flow and headless tools; do not extend the mock-login stack.
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### 1. Install in Cursor
|
|
44
|
+
|
|
45
|
+
Run this once:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx opentestmcp install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That writes an `opentest` MCP server into `~/.cursor/mcp.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"opentest": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": ["-y", "opentestmcp"],
|
|
59
|
+
"env": {
|
|
60
|
+
"OPENTEST_API_KEY": "ot_live_YOUR_API_KEY_HERE",
|
|
61
|
+
"OPENTEST_BACKEND": "https://flow.opentest.live",
|
|
62
|
+
"OPENTEST_FRONTEND": "https://www.flowtest.opentest.live"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Replace `ot_live_YOUR_API_KEY_HERE` with your OpenTest API key, then restart Cursor or toggle the MCP server off/on.
|
|
70
|
+
|
|
71
|
+
You can also pass the key directly:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx opentestmcp install --api-key ot_live_xxx
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For a project-local config, run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx opentestmcp install --project
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Add to Claude Code
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
claude mcp add opentest -- npx -y opentest
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Add to Cursor manually
|
|
90
|
+
|
|
91
|
+
Add to `.cursor/mcp.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"opentest": {
|
|
97
|
+
"command": "npx",
|
|
98
|
+
"args": ["-y", "opentestmcp"],
|
|
99
|
+
"env": {
|
|
100
|
+
"OPENTEST_API_KEY": "ot_live_YOUR_API_KEY_HERE"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 4. Authenticate (one time)
|
|
108
|
+
|
|
109
|
+
No manual API key needed. Just ask your agent:
|
|
110
|
+
|
|
111
|
+
> "Log in to OpenTest"
|
|
112
|
+
|
|
113
|
+
This runs the `opentest_login` tool which:
|
|
114
|
+
1. Starts a device authorization flow
|
|
115
|
+
2. Opens a browser link where you approve access
|
|
116
|
+
3. Stores the API key locally at `~/.opentest/credentials.json`
|
|
117
|
+
|
|
118
|
+
You only need to do this once — credentials persist across sessions. You can also set `OPENTEST_API_KEY` in the env config if you prefer manual key management.
|
|
119
|
+
|
|
120
|
+
### 5. Install the Chrome extension (optional)
|
|
121
|
+
|
|
122
|
+
For browser testing, load the extension:
|
|
123
|
+
|
|
124
|
+
1. Open `chrome://extensions`
|
|
125
|
+
2. Enable "Developer mode"
|
|
126
|
+
3. Click "Load unpacked"
|
|
127
|
+
4. Select the `browser/` folder from this package
|
|
128
|
+
|
|
129
|
+
### 6. Start testing
|
|
130
|
+
|
|
131
|
+
In your IDE, just ask:
|
|
132
|
+
|
|
133
|
+
> "Test the signup flow on localhost:3000"
|
|
134
|
+
|
|
135
|
+
> "Test POST /api/users with email test@example.com"
|
|
136
|
+
|
|
137
|
+
> "Run fullstack test: signup UI + POST /api/users in parallel"
|
|
138
|
+
|
|
139
|
+
## Inline Visual Results (MCP Apps)
|
|
140
|
+
|
|
141
|
+
When you call OpenTest tools from supported clients (Claude, VS Code, Goose), you'll see interactive results rendered directly in the conversation:
|
|
142
|
+
|
|
143
|
+
- **test_api** -- Editable API console in the IDE: method, full URL, headers/body JSON, response, inferred schema, and re-send without a new chat message
|
|
144
|
+
- **test_endpoint_by_id** -- Same as test_api but keyed by registry UUID from get_endpoints; optional body for login payloads
|
|
145
|
+
- **get_endpoints** -- Structured JSON endpoint registry; use `open_endpoints_ui` for the visual dashboard (grouped by tested/needed/draft where the client shows the inline panel)
|
|
146
|
+
- **test_flow** -- Step-by-step browser test progress; the MCP server also opens the **starting URL** in your default browser so you can watch the live tab (Hanzi extension). Inline panel shows task + results.
|
|
147
|
+
- **import_collection** (Postman / OpenAPI / Bruno / curl) -- Inline panel: generated `.ot.yaml`, per-endpoint request/response schemas when present, dashboard id when saved
|
|
148
|
+
- **generate_tests** -- Generated YAML, scenario counts, and save instructions
|
|
149
|
+
- **get_coverage** -- Coverage score, top gaps, and summary
|
|
150
|
+
- **detect_drift** -- Drift rows with severity and assertion/connection details
|
|
151
|
+
|
|
152
|
+
These tools attach `_meta` with a `ui://` resource (`collection-inspector.html`) so the IDE can render the panel (same MCP Apps pattern as `test_api`).
|
|
153
|
+
|
|
154
|
+
- **preview_login_mock** — **IDE login / Postman panel** (real `fetch` to your API): set **`api_base`** + **`login_path`**, POST JSON `{ email, password }`, see status/body and inferred shape. Not a mock server—only the form is a small static page. Pair with **`test_api`** for headless-only. Example: `api_base: "http://127.0.0.1:8000"`, `login_path: "/auth/login"`, then **Send login request** or let the agent navigate + click in **Agent Browser**.
|
|
155
|
+
|
|
156
|
+
**Cursor — inline browser (recommended):** Enable **Agent Browser** ([Cursor docs: Agent → Browser](https://cursor.com/docs/agent/browser)). After `preview_login_mock`, the tool returns **`mock_login_http_url`**; the agent should use **Browser → navigate** to that URL so the mock login opens **inside Cursor** (tab/pane), not only in external Chrome.
|
|
157
|
+
|
|
158
|
+
**If Browser tools do not appear:** Open **Cursor Settings**, enable the **Browser / Agent Browser** integration (enterprise: admins may need to toggle Browser under MCP). Then ask again to navigate to `mock_login_http_url`.
|
|
159
|
+
|
|
160
|
+
**Cursor:** `preview_login_mock` does **not** open Safari/Chrome by default (only the **Agent Browser** via `browser_navigate` to `mock_login_http_url`). Set **`OPENTEST_OPEN_MOCK_LOGIN=1`** if you also want the **OS default browser** tab. Or run `npm run open-mock-login` / open `dist/ui/src/ui/mock-login-lite.html` manually after `npm run build`.
|
|
161
|
+
|
|
162
|
+
The inline MCP UI panel and Cursor Agent Browser (`browser_navigate` on `ui.url`) are the supported surfaces — the MCP server no longer auto-opens system or Playwright browser windows.
|
|
163
|
+
|
|
164
|
+
Click **Re-send from editor** (or **Send**) to re-run API tests without typing a new message.
|
|
165
|
+
|
|
166
|
+
### Supported Clients
|
|
167
|
+
- Claude (web + desktop)
|
|
168
|
+
- VS Code (GitHub Copilot)
|
|
169
|
+
- Goose
|
|
170
|
+
- ChatGPT
|
|
171
|
+
|
|
172
|
+
## MCP Tools
|
|
173
|
+
|
|
174
|
+
| Tool | What it does |
|
|
175
|
+
|------|-------------|
|
|
176
|
+
| `opentest_login` | One-time device auth — opens browser link, stores API key locally |
|
|
177
|
+
| `opentest_login_complete` | Finishes the login flow after browser approval |
|
|
178
|
+
| `opentest_status` | Check authentication status and connection info |
|
|
179
|
+
| `opentest_logout` | Clear stored credentials and log out |
|
|
180
|
+
| `test_flow` | Test a UI flow in your real Chrome browser |
|
|
181
|
+
| `test_api` | Test an API endpoint with assertions |
|
|
182
|
+
| `test_fullstack` | Run UI + API tests in parallel |
|
|
183
|
+
| `get_endpoints` | Structured JSON inventory of endpoints (registry, sessions, collections) |
|
|
184
|
+
| `endpoints_dashboard` | Aggregated endpoint summary from all sources (structured JSON) |
|
|
185
|
+
| `open_endpoints_ui` | Open the visual endpoints dashboard in the browser |
|
|
186
|
+
| `need_endpoint` | Declare an endpoint the frontend needs |
|
|
187
|
+
| `browser_screenshot` | Capture the current browser page |
|
|
188
|
+
| `import_api_spec` | Import OpenAPI/Swagger/Postman specs |
|
|
189
|
+
| `auto_test` | Auto-discover and test all API endpoints |
|
|
190
|
+
|
|
191
|
+
## CLI Options
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
npx opentestmcp # Start — uses stored credentials or OPENTEST_API_KEY
|
|
195
|
+
npx opentestmcp --local # Local only, no dashboard sync
|
|
196
|
+
npx opentestmcp install # Install Cursor MCP config
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Environment variables:
|
|
200
|
+
|
|
201
|
+
- `OPENTEST_API_KEY` -- Manual API key override (skips device auth / stored credentials)
|
|
202
|
+
- `OPENTEST_BACKEND` -- Backend URL (default: `https://flow.opentest.live`)
|
|
203
|
+
- `OPENTEST_FRONTEND` -- Frontend URL (default: `https://www.flowtest.opentest.live`; use `www` to match the live site and avoid apex/`www` redirect issues in embedded browsers)
|
|
204
|
+
|
|
205
|
+
Tool responses and **`endpoints_page_url`** use the SPA entry: `/?agent_session=<uuid>&ot_agent_route=/endpoints` (and similar routes) so the first request always loads `index.html` on static hosting. The Flow app should read **`ot_agent_route`** on boot and run client-side **`navigate()`** to that path.
|
|
206
|
+
|
|
207
|
+
## Dashboard (Optional)
|
|
208
|
+
|
|
209
|
+
Sign up at [flow.opentest.live](https://flow.opentest.live) for:
|
|
210
|
+
- Team endpoint registry
|
|
211
|
+
- Test history and video recordings
|
|
212
|
+
- Jira/Slack integration
|
|
213
|
+
- Scheduled test runs
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
OPENTEST_API_KEY=ot_live_your_api_key npx opentestmcp
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Python MCP Server
|
|
220
|
+
|
|
221
|
+
This package also includes a Python MCP server for API-only testing via `uvx`:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uvx opentest-mcp --project-dir ./
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
See `pyproject.toml` for Python package details.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenTest CLI.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx opentestmcp install
|
|
8
|
+
* npx opentestmcp install --api-key ot_live_xxx
|
|
9
|
+
* npx opentestmcp
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = path.join(__dirname, "..");
|
|
20
|
+
const DEFAULT_BACKEND = "https://flow.opentest.live";
|
|
21
|
+
const DEFAULT_FRONTEND = "https://www.flowtest.opentest.live";
|
|
22
|
+
const API_KEY_PLACEHOLDER = "ot_live_YOUR_API_KEY_HERE";
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const command = args[0] === "--help" || args[0] === "-h"
|
|
26
|
+
? "help"
|
|
27
|
+
: args[0] && !args[0].startsWith("-")
|
|
28
|
+
? args[0]
|
|
29
|
+
: "start";
|
|
30
|
+
const commandArgs = command === "start" ? args : args.slice(1);
|
|
31
|
+
|
|
32
|
+
function argValue(name, fallback = "") {
|
|
33
|
+
const index = commandArgs.indexOf(name);
|
|
34
|
+
if (index === -1) return fallback;
|
|
35
|
+
return commandArgs[index + 1] || fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasFlag(name) {
|
|
39
|
+
return commandArgs.includes(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
process.stdout.write(`OpenTest MCP
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
npx opentestmcp install [options] Install OpenTest into Cursor MCP config
|
|
47
|
+
npx opentestmcp Start the OpenTest MCP server
|
|
48
|
+
|
|
49
|
+
Install options:
|
|
50
|
+
--api-key <key> OpenTest API key. Defaults to ${API_KEY_PLACEHOLDER}
|
|
51
|
+
--backend <url> OpenTest backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
52
|
+
--frontend <url> OpenTest frontend URL. Defaults to ${DEFAULT_FRONTEND}
|
|
53
|
+
--name <name> MCP server name. Defaults to opentest
|
|
54
|
+
--project Write .cursor/mcp.json in the current project
|
|
55
|
+
--global Write ~/.cursor/mcp.json (default)
|
|
56
|
+
--print Print the config instead of writing it
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
npx opentestmcp install
|
|
60
|
+
npx opentestmcp install --api-key ot_live_xxx
|
|
61
|
+
npx opentestmcp install --project --backend http://127.0.0.1:8000
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonFile(filePath) {
|
|
66
|
+
if (!existsSync(filePath)) return {};
|
|
67
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
68
|
+
if (!raw) return {};
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`Could not parse ${filePath}: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildServerConfig() {
|
|
77
|
+
const apiKey = argValue("--api-key", process.env.OPENTEST_API_KEY || API_KEY_PLACEHOLDER);
|
|
78
|
+
const backend = argValue("--backend", process.env.OPENTEST_BACKEND || DEFAULT_BACKEND);
|
|
79
|
+
const frontend = argValue("--frontend", process.env.OPENTEST_FRONTEND || DEFAULT_FRONTEND);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
command: "npx",
|
|
83
|
+
args: ["-y", "opentestmcp"],
|
|
84
|
+
env: {
|
|
85
|
+
OPENTEST_API_KEY: apiKey,
|
|
86
|
+
OPENTEST_BACKEND: backend,
|
|
87
|
+
OPENTEST_FRONTEND: frontend,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function installCursorMcp() {
|
|
93
|
+
const serverName = argValue("--name", "opentest");
|
|
94
|
+
const projectInstall = hasFlag("--project");
|
|
95
|
+
const serverConfig = buildServerConfig();
|
|
96
|
+
|
|
97
|
+
if (hasFlag("--print")) {
|
|
98
|
+
process.stdout.write(`${JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2)}\n`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const configPath = projectInstall
|
|
103
|
+
? path.join(process.cwd(), ".cursor", "mcp.json")
|
|
104
|
+
: path.join(os.homedir(), ".cursor", "mcp.json");
|
|
105
|
+
|
|
106
|
+
const config = {
|
|
107
|
+
...readJsonFile(configPath),
|
|
108
|
+
};
|
|
109
|
+
config.mcpServers = {
|
|
110
|
+
...(config.mcpServers || {}),
|
|
111
|
+
[serverName]: serverConfig,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const rendered = `${JSON.stringify(config, null, 2)}\n`;
|
|
115
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
116
|
+
writeFileSync(configPath, rendered, { mode: 0o600 });
|
|
117
|
+
|
|
118
|
+
const usedPlaceholder = config.mcpServers[serverName].env.OPENTEST_API_KEY === API_KEY_PLACEHOLDER;
|
|
119
|
+
process.stdout.write(`OpenTest MCP installed in ${configPath}
|
|
120
|
+
|
|
121
|
+
Server name: ${serverName}
|
|
122
|
+
Backend: ${config.mcpServers[serverName].env.OPENTEST_BACKEND}
|
|
123
|
+
|
|
124
|
+
Next steps:
|
|
125
|
+
1. ${usedPlaceholder ? `Replace ${API_KEY_PLACEHOLDER} with your OpenTest API key in ${configPath}.` : "Your OpenTest API key was written to the MCP config."}
|
|
126
|
+
2. Restart Cursor or toggle the OpenTest MCP server off/on.
|
|
127
|
+
3. Ask your agent: "Use OpenTest to list my endpoints."
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function startServer() {
|
|
132
|
+
const serverPath = path.join(ROOT, "dist", "server.js");
|
|
133
|
+
if (!existsSync(serverPath)) {
|
|
134
|
+
process.stderr.write(`[opentest] MCP server not found at ${serverPath}. Run npm run build before publishing.\n`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const env = { ...process.env };
|
|
139
|
+
const apiKey = argValue("--api-key", argValue("--key", ""));
|
|
140
|
+
const backend = argValue("--backend", "");
|
|
141
|
+
const frontend = argValue("--frontend", "");
|
|
142
|
+
if (apiKey) env.OPENTEST_API_KEY = apiKey;
|
|
143
|
+
if (backend) env.OPENTEST_BACKEND = backend;
|
|
144
|
+
if (frontend) env.OPENTEST_FRONTEND = frontend;
|
|
145
|
+
if (hasFlag("--local")) env.OPENTEST_BACKEND = "";
|
|
146
|
+
|
|
147
|
+
const child = spawn(process.execPath, [serverPath, ...commandArgs], {
|
|
148
|
+
env,
|
|
149
|
+
stdio: "inherit",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
child.on("exit", (code, signal) => {
|
|
153
|
+
if (signal) process.kill(process.pid, signal);
|
|
154
|
+
process.exit(code || 0);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (command === "install") {
|
|
159
|
+
installCursorMcp();
|
|
160
|
+
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
161
|
+
printHelp();
|
|
162
|
+
} else if (command === "start") {
|
|
163
|
+
startServer();
|
|
164
|
+
} else {
|
|
165
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
166
|
+
printHelp();
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Browser Extension
|
|
2
|
+
|
|
3
|
+
The Chrome extension and relay server are in the `opentest-browser/` directory at the repo root.
|
|
4
|
+
|
|
5
|
+
To load the extension:
|
|
6
|
+
1. Open chrome://extensions
|
|
7
|
+
2. Enable Developer mode
|
|
8
|
+
3. Click "Load unpacked"
|
|
9
|
+
4. Select the `opentest-browser/` directory
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const MCP_PREVIEW_RESOURCE_URI = "ui://opentest/mcp-preview";
|
|
2
|
+
export declare const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
3
|
+
export declare function escapeHtmlAttr(s: string): string;
|
|
4
|
+
export declare function escapeHtmlText(s: string): string;
|
|
5
|
+
export interface PreviewTool {
|
|
6
|
+
name?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
inputSchema?: unknown;
|
|
9
|
+
_endpoint_ref?: {
|
|
10
|
+
method?: string;
|
|
11
|
+
path_template?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
source?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface PreviewPayload {
|
|
17
|
+
intent?: string | string[];
|
|
18
|
+
selection_method?: string | string[];
|
|
19
|
+
rationale?: string | string[] | Record<string, string>;
|
|
20
|
+
selected_count?: number;
|
|
21
|
+
spec_preview?: {
|
|
22
|
+
upstream_base_url?: string | string[];
|
|
23
|
+
tools?: PreviewTool[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function buildConversionPanelHtml(data: PreviewPayload): string;
|
|
27
|
+
export declare function writeMcpPreviewFile(panelHtml: string): Promise<{
|
|
28
|
+
absolutePath: string;
|
|
29
|
+
fileUrl: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-pane HTML for mcp_preview — shared by the MCP stdio server and
|
|
3
|
+
* scripts/emit-mcp-preview.mjs (browser tab fallback when Cursor does not render mcp-app).
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
export const MCP_PREVIEW_RESOURCE_URI = "ui://opentest/mcp-preview";
|
|
9
|
+
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
10
|
+
export function escapeHtmlAttr(s) {
|
|
11
|
+
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
12
|
+
}
|
|
13
|
+
export function escapeHtmlText(s) {
|
|
14
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
15
|
+
}
|
|
16
|
+
export function buildConversionPanelHtml(data) {
|
|
17
|
+
const toStr = (v) => {
|
|
18
|
+
if (v == null)
|
|
19
|
+
return "";
|
|
20
|
+
if (Array.isArray(v))
|
|
21
|
+
return v.filter((x) => x != null).map((x) => String(x)).join("; ");
|
|
22
|
+
if (typeof v === "object") {
|
|
23
|
+
try {
|
|
24
|
+
return Object.entries(v)
|
|
25
|
+
.map(([k, val]) => `${k}: ${val == null ? "" : String(val)}`)
|
|
26
|
+
.join(" · ");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(v);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return String(v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return String(v);
|
|
38
|
+
};
|
|
39
|
+
const intent = toStr(data.intent).trim() || "(unspecified)";
|
|
40
|
+
const method = toStr(data.selection_method).trim() || "unknown";
|
|
41
|
+
const rationale = toStr(data.rationale).trim();
|
|
42
|
+
const upstream = toStr(data.spec_preview?.upstream_base_url).trim();
|
|
43
|
+
const tools = Array.isArray(data.spec_preview?.tools) ? data.spec_preview.tools : [];
|
|
44
|
+
const selectedCount = typeof data.selected_count === "number" ? data.selected_count : tools.length;
|
|
45
|
+
const linkKey = (m, p) => `${(m || "").toUpperCase()} ${p || ""}`.trim();
|
|
46
|
+
const endpointRows = tools
|
|
47
|
+
.map((t) => {
|
|
48
|
+
const ref = t._endpoint_ref ?? {};
|
|
49
|
+
const m = (ref.method ?? "").toUpperCase();
|
|
50
|
+
const p = ref.path_template ?? "";
|
|
51
|
+
const key = linkKey(m, p);
|
|
52
|
+
const tags = Array.isArray(ref.tags) && ref.tags.length > 0 ? ref.tags.join(", ") : "";
|
|
53
|
+
return `
|
|
54
|
+
<div class="row" data-link="${escapeHtmlAttr(key)}">
|
|
55
|
+
<div class="row-head">
|
|
56
|
+
<span class="method ${escapeHtmlAttr(m)}">${escapeHtmlText(m)}</span>
|
|
57
|
+
<span class="path">${escapeHtmlText(p)}</span>
|
|
58
|
+
</div>
|
|
59
|
+
${tags ? `<div class="row-meta">tags: ${escapeHtmlText(tags)}</div>` : ""}
|
|
60
|
+
</div>`;
|
|
61
|
+
})
|
|
62
|
+
.join("");
|
|
63
|
+
const toolRows = tools
|
|
64
|
+
.map((t) => {
|
|
65
|
+
const ref = t._endpoint_ref ?? {};
|
|
66
|
+
const key = linkKey(ref.method, ref.path_template);
|
|
67
|
+
const schemaJson = (() => {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.stringify(t.inputSchema ?? {}, null, 2);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return "{}";
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
return `
|
|
76
|
+
<div class="row" data-link="${escapeHtmlAttr(key)}">
|
|
77
|
+
<div class="tool-name">${escapeHtmlText(t.name ?? "(unnamed)")}</div>
|
|
78
|
+
${t.description ? `<div class="tool-desc">${escapeHtmlText(t.description)}</div>` : ""}
|
|
79
|
+
<pre class="tool-schema">${escapeHtmlText(schemaJson)}</pre>
|
|
80
|
+
</div>`;
|
|
81
|
+
})
|
|
82
|
+
.join("");
|
|
83
|
+
const empty = tools.length === 0
|
|
84
|
+
? `<div class="empty">No matching endpoints. Try a different intent or run <code>verify_endpoints_live</code> first.</div>`
|
|
85
|
+
: "";
|
|
86
|
+
return `<!DOCTYPE html>
|
|
87
|
+
<html lang="en">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="UTF-8" />
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
91
|
+
<title>OpenTest · MCP Preview</title>
|
|
92
|
+
<style>
|
|
93
|
+
:root { color-scheme: light dark; --bg:#0d1117; --bg2:#161b22; --border:#30363d; --fg:#e6edf3; --muted:#8b949e; --accent:#58a6ff; --green:#2ea043; --orange:#bf8700; --red:#cf222e; --purple:#8957e5; }
|
|
94
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
95
|
+
html, body { height:100%; background:var(--bg); color:var(--fg); font-family:-apple-system,BlinkMacSystemFont,sans-serif; font-size:13px; }
|
|
96
|
+
body { display:flex; flex-direction:column; overflow:hidden; }
|
|
97
|
+
header { padding:10px 14px; border-bottom:1px solid var(--border); background:var(--bg2); flex-shrink:0; }
|
|
98
|
+
header h1 { font-size:13px; font-weight:600; color:var(--accent); }
|
|
99
|
+
header .meta { font-size:11px; color:var(--muted); margin-top:3px; }
|
|
100
|
+
header .meta strong { color:var(--fg); font-weight:600; }
|
|
101
|
+
header .rationale { font-size:11px; color:var(--muted); margin-top:5px; font-style:italic; max-width:900px; }
|
|
102
|
+
.columns { flex:1; display:grid; grid-template-columns:1fr 1fr; gap:1px; background:var(--border); overflow:hidden; min-height:0; }
|
|
103
|
+
.col { background:var(--bg); overflow-y:auto; }
|
|
104
|
+
.col-header { padding:7px 12px; font-size:10px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--muted); border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg2); z-index:1; }
|
|
105
|
+
.row { padding:9px 12px; border-bottom:1px solid var(--border); transition:background 0.1s; }
|
|
106
|
+
.row:last-child { border-bottom:none; }
|
|
107
|
+
.row:hover, .row.linked { background:rgba(88,166,255,0.08); }
|
|
108
|
+
.row-head { display:flex; align-items:center; gap:8px; }
|
|
109
|
+
.row-meta { font-size:10px; color:var(--muted); margin-top:3px; padding-left:56px; }
|
|
110
|
+
.method { display:inline-block; min-width:48px; text-align:center; padding:2px 6px; border-radius:3px; font-weight:700; font-size:10px; color:white; font-family:SFMono-Regular,Consolas,monospace; }
|
|
111
|
+
.method.GET{background:var(--accent);} .method.POST{background:var(--green);} .method.PATCH{background:var(--orange);} .method.PUT{background:var(--purple);} .method.DELETE{background:var(--red);}
|
|
112
|
+
.path { font-family:SFMono-Regular,Consolas,monospace; font-size:12px; word-break:break-all; }
|
|
113
|
+
.tool-name { font-family:SFMono-Regular,Consolas,monospace; font-size:12px; font-weight:600; color:var(--accent); }
|
|
114
|
+
.tool-desc { font-size:11px; color:var(--muted); margin-top:3px; line-height:1.4; }
|
|
115
|
+
.tool-schema { font-family:SFMono-Regular,Consolas,monospace; font-size:10px; background:var(--bg2); padding:6px 8px; border-radius:3px; margin-top:6px; white-space:pre-wrap; word-break:break-all; max-height:120px; overflow-y:auto; line-height:1.4; color:var(--muted); }
|
|
116
|
+
.empty { padding:24px; text-align:center; color:var(--muted); font-style:italic; }
|
|
117
|
+
.empty code { font-family:SFMono-Regular,Consolas,monospace; color:var(--accent); background:var(--bg2); padding:1px 5px; border-radius:3px; font-style:normal; }
|
|
118
|
+
footer { padding:8px 14px; border-top:1px solid var(--border); background:var(--bg2); display:flex; gap:10px; align-items:center; flex-shrink:0; font-size:11px; color:var(--muted); }
|
|
119
|
+
footer .upstream { font-family:SFMono-Regular,Consolas,monospace; color:var(--accent); }
|
|
120
|
+
footer .deploy-hint { margin-left:auto; }
|
|
121
|
+
footer .deploy-hint code { font-family:SFMono-Regular,Consolas,monospace; color:var(--fg); background:var(--bg); padding:2px 6px; border-radius:3px; }
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<header>
|
|
126
|
+
<h1>API → MCP Conversion preview</h1>
|
|
127
|
+
<div class="meta">
|
|
128
|
+
Intent: <strong>${escapeHtmlText(intent)}</strong> · Selected <strong>${selectedCount}</strong> endpoint${selectedCount === 1 ? "" : "s"} · Method: ${escapeHtmlText(method)}
|
|
129
|
+
</div>
|
|
130
|
+
${rationale ? `<div class="rationale">${escapeHtmlText(rationale)}</div>` : ""}
|
|
131
|
+
</header>
|
|
132
|
+
<main class="columns">
|
|
133
|
+
<div class="col">
|
|
134
|
+
<div class="col-header">Your API endpoints (${tools.length})</div>
|
|
135
|
+
${endpointRows}${tools.length === 0 ? empty : ""}
|
|
136
|
+
</div>
|
|
137
|
+
<div class="col">
|
|
138
|
+
<div class="col-header">Generated MCP tools (${tools.length})</div>
|
|
139
|
+
${toolRows}${tools.length === 0 ? empty : ""}
|
|
140
|
+
</div>
|
|
141
|
+
</main>
|
|
142
|
+
<footer>
|
|
143
|
+
<span>Upstream: <span class="upstream">${escapeHtmlText(upstream || "(not set)")}</span></span>
|
|
144
|
+
<span class="deploy-hint">Next: ask the agent to call <code>mcp_deploy</code></span>
|
|
145
|
+
</footer>
|
|
146
|
+
<script>
|
|
147
|
+
document.querySelectorAll('.row[data-link]').forEach(row => {
|
|
148
|
+
const key = row.getAttribute('data-link');
|
|
149
|
+
if (!key) return;
|
|
150
|
+
const matches = () => document.querySelectorAll('[data-link="' + CSS.escape(key) + '"]');
|
|
151
|
+
row.addEventListener('mouseenter', () => matches().forEach(r => r.classList.add('linked')));
|
|
152
|
+
row.addEventListener('mouseleave', () => matches().forEach(r => r.classList.remove('linked')));
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
155
|
+
</body>
|
|
156
|
+
</html>`;
|
|
157
|
+
}
|
|
158
|
+
export async function writeMcpPreviewFile(panelHtml) {
|
|
159
|
+
const outPath = path.join(process.cwd(), "opentest-mcp", "mcp-preview-last.html");
|
|
160
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
161
|
+
await fs.writeFile(outPath, panelHtml, "utf8");
|
|
162
|
+
return {
|
|
163
|
+
absolutePath: outPath,
|
|
164
|
+
fileUrl: pathToFileURL(outPath).href,
|
|
165
|
+
};
|
|
166
|
+
}
|
package/dist/server.d.ts
ADDED