premanmcp 0.3.4 → 0.4.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/README.md +75 -21
- package/bin/cli.js +345 -33
- package/bin/hosted.js +455 -0
- package/dist/auth_flow_ui.d.ts +3 -0
- package/dist/auth_flow_ui.js +13 -2
- package/dist/server.js +219 -22
- package/dist/user_auth_flow.js +28 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,19 +7,24 @@ PreMan is agent-first API infrastructure. It lets backend teams expose endpoints
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
npm exec -y premanmcp@latest -- install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Local development form:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node bin/cli.js install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
First-time users are prompted for email, OTP, and password directly in the terminal. PreMan creates or connects the account, generates an API key, saves it to `~/.preman/credentials.json`, then writes a `preman` MCP server into `~/.cursor/mcp.json`:
|
|
14
20
|
|
|
15
21
|
```json
|
|
16
22
|
{
|
|
17
23
|
"mcpServers": {
|
|
18
24
|
"preman": {
|
|
19
|
-
"command": "
|
|
20
|
-
"args": ["-y", "premanmcp"],
|
|
25
|
+
"command": "npm",
|
|
26
|
+
"args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
|
|
21
27
|
"env": {
|
|
22
|
-
"PREMAN_API_KEY": "pm_live_YOUR_API_KEY_HERE",
|
|
23
28
|
"PREMAN_BACKEND": "https://api.preman.live",
|
|
24
29
|
"PREMAN_FRONTEND": "https://app.preman.live"
|
|
25
30
|
}
|
|
@@ -28,35 +33,47 @@ This writes an `preman` MCP server into `~/.cursor/mcp.json`:
|
|
|
28
33
|
}
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
Restart Cursor or toggle the MCP server off/on after install. The MCP server loads the saved key automatically.
|
|
37
|
+
|
|
38
|
+
You can also create or connect your account before install:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm exec -y premanmcp@latest -- login
|
|
42
|
+
```
|
|
32
43
|
|
|
33
44
|
You can also pass the key directly:
|
|
34
45
|
|
|
35
46
|
```bash
|
|
36
|
-
|
|
47
|
+
npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
|
|
37
48
|
```
|
|
38
49
|
|
|
39
50
|
For project-local installation:
|
|
40
51
|
|
|
41
52
|
```bash
|
|
42
|
-
|
|
53
|
+
npm exec -y premanmcp@latest -- install --project
|
|
43
54
|
```
|
|
44
55
|
|
|
45
56
|
## What It Does
|
|
46
57
|
|
|
47
58
|
- Converts API endpoints into agent-callable MCP tools.
|
|
59
|
+
- Creates/connects a PreMan account from the terminal or IDE agent.
|
|
48
60
|
- Lets agents test real backend endpoints from the IDE.
|
|
49
61
|
- Syncs endpoint inventory across backend and frontend workflows.
|
|
50
62
|
- Adds API-key auth for PreMan MCP access.
|
|
51
63
|
- Supports hosted MCPs with consumer tokens for customer-facing agent access.
|
|
52
64
|
- Records per-call observability so teams can audit which agent did what.
|
|
65
|
+
- Hands failing-endpoint alerts to your agent as fix tasks (`preman_get_fix_task` → repro curl → `preman_complete_fix_task`).
|
|
53
66
|
|
|
54
67
|
## Common Agent Commands
|
|
55
68
|
|
|
56
69
|
After installing, ask your coding agent:
|
|
57
70
|
|
|
58
71
|
```text
|
|
59
|
-
Use
|
|
72
|
+
Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
Use PreMan to sign me up, verify my email OTP, create my API key, then scan this project.
|
|
60
77
|
```
|
|
61
78
|
|
|
62
79
|
```text
|
|
@@ -71,12 +88,16 @@ Convert these endpoints into an MCP.
|
|
|
71
88
|
Show me the audit log for this hosted MCP.
|
|
72
89
|
```
|
|
73
90
|
|
|
91
|
+
```text
|
|
92
|
+
Pull my pending PreMan fix tasks and fix the failing endpoint.
|
|
93
|
+
```
|
|
94
|
+
|
|
74
95
|
## Cursor
|
|
75
96
|
|
|
76
97
|
The installer targets Cursor by default:
|
|
77
98
|
|
|
78
99
|
```bash
|
|
79
|
-
|
|
100
|
+
npm exec -y premanmcp@latest -- install
|
|
80
101
|
```
|
|
81
102
|
|
|
82
103
|
Manual Cursor config:
|
|
@@ -85,11 +106,8 @@ Manual Cursor config:
|
|
|
85
106
|
{
|
|
86
107
|
"mcpServers": {
|
|
87
108
|
"preman": {
|
|
88
|
-
"command": "
|
|
89
|
-
"args": ["-y", "premanmcp"]
|
|
90
|
-
"env": {
|
|
91
|
-
"PREMAN_API_KEY": "pm_live_YOUR_API_KEY_HERE"
|
|
92
|
-
}
|
|
109
|
+
"command": "npm",
|
|
110
|
+
"args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"]
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
}
|
|
@@ -98,17 +116,18 @@ Manual Cursor config:
|
|
|
98
116
|
## Claude Code
|
|
99
117
|
|
|
100
118
|
```bash
|
|
101
|
-
claude mcp add preman --
|
|
119
|
+
claude mcp add preman -- npm exec -y --package premanmcp@latest -- premanmcp
|
|
102
120
|
```
|
|
103
121
|
|
|
104
122
|
## CLI
|
|
105
123
|
|
|
106
124
|
```bash
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
npm exec -y premanmcp@latest -- # Start the MCP server
|
|
126
|
+
npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
|
|
127
|
+
npm exec -y premanmcp@latest -- install # Install Cursor MCP config
|
|
128
|
+
npm exec -y premanmcp@latest -- install --project # Write .cursor/mcp.json in current repo
|
|
129
|
+
npm exec -y premanmcp@latest -- install --print # Print config without writing
|
|
130
|
+
npm exec -y premanmcp@latest -- install --api-key KEY # Install with your PreMan API key
|
|
112
131
|
```
|
|
113
132
|
|
|
114
133
|
Options:
|
|
@@ -118,13 +137,48 @@ Options:
|
|
|
118
137
|
- `--frontend <url>`: PreMan frontend URL. Defaults to `https://app.preman.live`.
|
|
119
138
|
- `--name <name>`: MCP server name. Defaults to `preman`.
|
|
120
139
|
- `--project`: Write `.cursor/mcp.json` in the current project.
|
|
140
|
+
- `--skip-login`: Install config without interactive terminal auth.
|
|
121
141
|
- `--print`: Print the generated MCP config without writing it.
|
|
122
142
|
|
|
143
|
+
## Run a hosted MCP from the terminal
|
|
144
|
+
|
|
145
|
+
Installing the package also gives you a `preman` command. Once a selection is published as a
|
|
146
|
+
hosted MCP, you can call its tools straight from a shell — same published selection, same
|
|
147
|
+
consumer token, and same audit trail as an agent calling `POST /h/<id>/mcp`. Updating the
|
|
148
|
+
selection updates the terminal; there is nothing to re-install.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
preman link https://api.preman.live/h/<id>/mcp --env staging --token pm_hmcp_xxx
|
|
152
|
+
preman tools --env staging
|
|
153
|
+
preman run post_users_id_orders --env staging --arg id=42 --json '{"body":{"sku":"A1"}}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`link` takes the URL straight from the deploy install snippet (a bare hosted MCP id works too,
|
|
157
|
+
with `--backend`). Profiles are stored in `~/.preman/cli.json` (mode 0600); `--env <name>`
|
|
158
|
+
picks one. Tool names are the ones the runtime publishes — run `preman tools` to see them.
|
|
159
|
+
|
|
160
|
+
Run options:
|
|
161
|
+
|
|
162
|
+
- `--env <name>`: Profile to use. Falls back to `PREMAN_MCP_URL`, then the default profile.
|
|
163
|
+
- `--arg key=value`: String argument (repeatable). `--arg key:=json` sends a JSON-typed value.
|
|
164
|
+
- `--json '{...}'`: Full arguments object. `--json -` reads it from stdin.
|
|
165
|
+
- `--timeout <seconds>`: Client-side timeout. Defaults to 60.
|
|
166
|
+
- `--json-out`: Print the raw JSON-RPC result.
|
|
167
|
+
- `<tool>` may be dotted — `preman run staging.get_orders` selects the profile inline.
|
|
168
|
+
|
|
169
|
+
Exit codes: `0` success, `1` the tool returned an error, `2` usage, `3` authentication,
|
|
170
|
+
`4` JSON-RPC error (e.g. unknown tool), `5` network failure.
|
|
171
|
+
|
|
123
172
|
## Environment Variables
|
|
124
173
|
|
|
125
174
|
- `PREMAN_API_KEY`: PreMan API key.
|
|
126
175
|
- `PREMAN_BACKEND`: PreMan backend URL.
|
|
127
176
|
- `PREMAN_FRONTEND`: PreMan frontend URL.
|
|
177
|
+
- `PREMAN_MCP_URL`: Hosted MCP endpoint (`…/h/<id>/mcp`) for `preman run`/`preman tools` when
|
|
178
|
+
`--env` is omitted — useful in CI, where no profile file exists.
|
|
179
|
+
- `PREMAN_MCP_TOKEN`: Consumer token for `PREMAN_MCP_URL`.
|
|
180
|
+
|
|
181
|
+
If `PREMAN_API_KEY` is omitted, the MCP server loads credentials from `~/.preman/credentials.json`.
|
|
128
182
|
|
|
129
183
|
## Product
|
|
130
184
|
|
package/bin/cli.js
CHANGED
|
@@ -4,22 +4,27 @@
|
|
|
4
4
|
* PreMan CLI.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* npm exec -y premanmcp@latest -- login
|
|
8
|
+
* npm exec -y premanmcp@latest -- install
|
|
9
|
+
* npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
|
|
10
|
+
* npm exec -y premanmcp@latest --
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { spawn } from "node:child_process";
|
|
13
14
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
15
|
import os from "node:os";
|
|
15
16
|
import path from "node:path";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
16
18
|
import { fileURLToPath } from "node:url";
|
|
17
19
|
|
|
20
|
+
import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
|
|
21
|
+
|
|
18
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
23
|
const ROOT = path.join(__dirname, "..");
|
|
20
24
|
const DEFAULT_BACKEND = "https://api.preman.live";
|
|
21
25
|
const DEFAULT_FRONTEND = "https://app.preman.live";
|
|
22
|
-
const
|
|
26
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), ".preman");
|
|
27
|
+
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
23
28
|
|
|
24
29
|
const args = process.argv.slice(2);
|
|
25
30
|
const command = args[0] === "--help" || args[0] === "-h"
|
|
@@ -43,23 +48,31 @@ function printHelp() {
|
|
|
43
48
|
process.stdout.write(`PreMan MCP
|
|
44
49
|
|
|
45
50
|
Usage:
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
|
|
52
|
+
npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
|
|
53
|
+
npm exec -y premanmcp@latest -- Start the PreMan MCP server
|
|
54
|
+
preman link|tools|run ... Drive a published hosted MCP
|
|
55
|
+
|
|
56
|
+
Login options:
|
|
57
|
+
--email <email> Pre-fill the email prompt
|
|
58
|
+
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
48
59
|
|
|
49
60
|
Install options:
|
|
50
|
-
--api-key <key> PreMan API key.
|
|
61
|
+
--api-key <key> PreMan API key. If omitted, stored CLI credentials are used
|
|
51
62
|
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
52
63
|
--frontend <url> PreMan frontend URL. Defaults to ${DEFAULT_FRONTEND}
|
|
53
64
|
--name <name> MCP server name. Defaults to preman
|
|
54
65
|
--project Write .cursor/mcp.json in the current project
|
|
55
66
|
--global Write ~/.cursor/mcp.json (default)
|
|
67
|
+
--skip-login Install config without interactive terminal auth
|
|
56
68
|
--print Print the config instead of writing it
|
|
57
69
|
|
|
58
70
|
Examples:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
npm exec -y premanmcp@latest -- login
|
|
72
|
+
npm exec -y premanmcp@latest -- install
|
|
73
|
+
npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
|
|
74
|
+
npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
|
|
75
|
+
${HOSTED_HELP}`);
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
function readJsonFile(filePath) {
|
|
@@ -73,25 +86,306 @@ function readJsonFile(filePath) {
|
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
function readStoredCredentials() {
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(CREDENTIALS_FILE, "utf8").trim();
|
|
92
|
+
if (!raw) return null;
|
|
93
|
+
const creds = JSON.parse(raw);
|
|
94
|
+
if (creds && typeof creds.api_key === "string" && creds.api_key.startsWith("pm_live_")) {
|
|
95
|
+
return creds;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// No stored credentials yet.
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function saveStoredCredentials(creds) {
|
|
104
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
105
|
+
writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function backendUrl() {
|
|
109
|
+
return argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function promptText(question) {
|
|
113
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
try {
|
|
115
|
+
return (await rl.question(question)).trim();
|
|
116
|
+
} finally {
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function promptSecret(question) {
|
|
122
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
123
|
+
return promptText(question);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const stdin = process.stdin;
|
|
128
|
+
const stdout = process.stdout;
|
|
129
|
+
const wasRaw = stdin.isRaw;
|
|
130
|
+
let value = "";
|
|
131
|
+
|
|
132
|
+
function cleanup() {
|
|
133
|
+
stdin.off("data", onData);
|
|
134
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
135
|
+
stdin.pause();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onData(chunk) {
|
|
139
|
+
const text = String(chunk);
|
|
140
|
+
if (text === "\u0003") {
|
|
141
|
+
stdout.write("\n");
|
|
142
|
+
cleanup();
|
|
143
|
+
process.exit(130);
|
|
144
|
+
}
|
|
145
|
+
if (text === "\r" || text === "\n" || text === "\u0004") {
|
|
146
|
+
stdout.write("\n");
|
|
147
|
+
cleanup();
|
|
148
|
+
resolve(value);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (text === "\u007f" || text === "\b") {
|
|
152
|
+
if (value.length) {
|
|
153
|
+
value = value.slice(0, -1);
|
|
154
|
+
stdout.write("\b \b");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
value += text;
|
|
159
|
+
stdout.write("*");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
stdout.write(question);
|
|
163
|
+
stdin.setRawMode(true);
|
|
164
|
+
stdin.resume();
|
|
165
|
+
stdin.setEncoding("utf8");
|
|
166
|
+
stdin.on("data", onData);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function promptPasswordTwice() {
|
|
171
|
+
const password = await promptSecret("Create password: ");
|
|
172
|
+
if (!password || password.length < 6) {
|
|
173
|
+
throw new Error("Password must be at least 6 characters.");
|
|
174
|
+
}
|
|
175
|
+
const confirm = await promptSecret("Confirm password: ");
|
|
176
|
+
if (password !== confirm) {
|
|
177
|
+
throw new Error("Passwords do not match.");
|
|
178
|
+
}
|
|
179
|
+
return password;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function callBackendJson(method, routePath, { json, token, query } = {}) {
|
|
183
|
+
const url = new URL(routePath.replace(/^\/+/, ""), `${backendUrl()}/`);
|
|
184
|
+
if (query) {
|
|
185
|
+
for (const [key, value] of Object.entries(query)) {
|
|
186
|
+
if (value != null && value !== "") url.searchParams.set(key, String(value));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const headers = { Accept: "application/json" };
|
|
191
|
+
const hasBody = json !== undefined && json !== null;
|
|
192
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
193
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
194
|
+
|
|
195
|
+
const resp = await fetch(url, {
|
|
196
|
+
method,
|
|
197
|
+
headers,
|
|
198
|
+
body: hasBody ? JSON.stringify(json) : undefined,
|
|
199
|
+
});
|
|
200
|
+
const text = await resp.text();
|
|
201
|
+
let body = {};
|
|
202
|
+
try {
|
|
203
|
+
body = text ? JSON.parse(text) : {};
|
|
204
|
+
} catch {
|
|
205
|
+
body = { raw: text };
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
status_code: resp.status,
|
|
209
|
+
ok: resp.ok,
|
|
210
|
+
...body,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertOk(result, action) {
|
|
215
|
+
if (result.ok) return;
|
|
216
|
+
const detail = result.detail || result.message || result.raw || `${action} failed`;
|
|
217
|
+
throw new Error(`${action} failed: ${result.status_code} ${detail}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function verifyUnconfirmedAccount(email) {
|
|
221
|
+
process.stdout.write("This email exists but is not verified. Sending a new OTP.\n");
|
|
222
|
+
const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
|
|
223
|
+
assertOk(resend, "resend OTP");
|
|
224
|
+
const otp = await promptText("Verification code: ");
|
|
225
|
+
const verified = await callBackendJson("POST", "/auth/verify-otp", { json: { email, otp } });
|
|
226
|
+
assertOk(verified, "verify OTP");
|
|
227
|
+
return verified.access_token;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function createAccountFromTerminal(email) {
|
|
231
|
+
const signup = await callBackendJson("POST", "/auth/start-signup", {
|
|
232
|
+
json: { email },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (signup.ok) {
|
|
236
|
+
process.stdout.write("Verification code sent. Check your email.\n");
|
|
237
|
+
const otp = await promptText("Verification code: ");
|
|
238
|
+
const password = await promptPasswordTwice();
|
|
239
|
+
const setPassword = await callBackendJson("POST", "/auth/set-password", {
|
|
240
|
+
json: { email, otp, new_password: password },
|
|
241
|
+
});
|
|
242
|
+
assertOk(setPassword, "set password");
|
|
243
|
+
return String(setPassword.access_token || "");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (signup.status_code !== 404) {
|
|
247
|
+
assertOk(signup, "start signup");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
process.stdout.write(
|
|
251
|
+
"This PreMan backend uses the password-first signup flow. Create your password now, then enter the email code.\n"
|
|
252
|
+
);
|
|
253
|
+
const password = await promptPasswordTwice();
|
|
254
|
+
const legacySignup = await callBackendJson("POST", "/auth/signup", {
|
|
255
|
+
json: { email, password },
|
|
256
|
+
});
|
|
257
|
+
assertOk(legacySignup, "signup");
|
|
258
|
+
process.stdout.write("Verification code sent. Check your email.\n");
|
|
259
|
+
const otp = await promptText("Verification code: ");
|
|
260
|
+
const verified = await callBackendJson("POST", "/auth/verify-otp", {
|
|
261
|
+
json: { email, otp },
|
|
262
|
+
});
|
|
263
|
+
assertOk(verified, "verify OTP");
|
|
264
|
+
return String(verified.access_token || "");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function authenticateTerminal() {
|
|
268
|
+
const explicitKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
|
|
269
|
+
if (explicitKey && explicitKey.startsWith("pm_live_")) {
|
|
270
|
+
const creds = {
|
|
271
|
+
api_key: explicitKey,
|
|
272
|
+
backend_url: backendUrl(),
|
|
273
|
+
user_email: argValue("--email", ""),
|
|
274
|
+
device_name: os.hostname(),
|
|
275
|
+
created_at: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
saveStoredCredentials(creds);
|
|
278
|
+
return creds;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const email = (argValue("--email", "") || await promptText("Email: ")).trim().toLowerCase();
|
|
282
|
+
if (!email) throw new Error("Email is required.");
|
|
283
|
+
|
|
284
|
+
process.stdout.write(`Checking PreMan account for ${email}...\n`);
|
|
285
|
+
const account = await callBackendJson("GET", "/auth/needs-password", { query: { email } });
|
|
286
|
+
assertOk(account, "check account");
|
|
287
|
+
|
|
288
|
+
let accessToken = "";
|
|
289
|
+
|
|
290
|
+
if (!account.exists) {
|
|
291
|
+
process.stdout.write("No PreMan account found. Creating one now.\n");
|
|
292
|
+
accessToken = await createAccountFromTerminal(email);
|
|
293
|
+
} else if (account.needs_password) {
|
|
294
|
+
process.stdout.write("This account needs a password. Sending an OTP first.\n");
|
|
295
|
+
const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
|
|
296
|
+
assertOk(resend, "resend OTP");
|
|
297
|
+
const otp = await promptText("Verification code: ");
|
|
298
|
+
const password = await promptPasswordTwice();
|
|
299
|
+
const setPassword = await callBackendJson("POST", "/auth/set-password", {
|
|
300
|
+
json: { email, otp, new_password: password },
|
|
301
|
+
});
|
|
302
|
+
assertOk(setPassword, "set password");
|
|
303
|
+
accessToken = String(setPassword.access_token || "");
|
|
304
|
+
} else {
|
|
305
|
+
const password = await promptSecret("Password: ");
|
|
306
|
+
const login = await callBackendJson("POST", "/auth/login", {
|
|
307
|
+
json: { email, password },
|
|
308
|
+
});
|
|
309
|
+
if (!login.ok && login.status_code === 403 && String(login.detail || "").toLowerCase().includes("not verified")) {
|
|
310
|
+
accessToken = await verifyUnconfirmedAccount(email);
|
|
311
|
+
} else {
|
|
312
|
+
assertOk(login, "login");
|
|
313
|
+
accessToken = String(login.access_token || "");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!accessToken) throw new Error("PreMan did not return an access token.");
|
|
318
|
+
|
|
319
|
+
const keyName = `PreMan MCP CLI (${os.hostname()})`;
|
|
320
|
+
const key = await callBackendJson("POST", "/api-keys", {
|
|
321
|
+
token: accessToken,
|
|
322
|
+
json: { name: keyName },
|
|
323
|
+
});
|
|
324
|
+
assertOk(key, "create API key");
|
|
325
|
+
if (!key.key || !String(key.key).startsWith("pm_live_")) {
|
|
326
|
+
throw new Error("PreMan did not return a valid API key.");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const creds = {
|
|
330
|
+
api_key: String(key.key),
|
|
331
|
+
backend_url: backendUrl(),
|
|
332
|
+
user_email: email,
|
|
333
|
+
device_name: os.hostname(),
|
|
334
|
+
created_at: new Date().toISOString(),
|
|
335
|
+
};
|
|
336
|
+
saveStoredCredentials(creds);
|
|
337
|
+
return creds;
|
|
338
|
+
}
|
|
339
|
+
|
|
76
340
|
function buildServerConfig() {
|
|
77
|
-
const apiKey = argValue("--api-key", process.env.PREMAN_API_KEY ||
|
|
341
|
+
const apiKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
|
|
78
342
|
const backend = argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND);
|
|
79
343
|
const frontend = argValue("--frontend", process.env.PREMAN_FRONTEND || DEFAULT_FRONTEND);
|
|
344
|
+
const env = {
|
|
345
|
+
PREMAN_BACKEND: backend,
|
|
346
|
+
PREMAN_FRONTEND: frontend,
|
|
347
|
+
};
|
|
348
|
+
if (apiKey) {
|
|
349
|
+
env.PREMAN_API_KEY = apiKey;
|
|
350
|
+
}
|
|
80
351
|
|
|
81
352
|
return {
|
|
82
|
-
command: "
|
|
83
|
-
args: ["-y", "premanmcp"],
|
|
84
|
-
env
|
|
85
|
-
PREMAN_API_KEY: apiKey,
|
|
86
|
-
PREMAN_BACKEND: backend,
|
|
87
|
-
PREMAN_FRONTEND: frontend,
|
|
88
|
-
},
|
|
353
|
+
command: "npm",
|
|
354
|
+
args: ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
|
|
355
|
+
env,
|
|
89
356
|
};
|
|
90
357
|
}
|
|
91
358
|
|
|
92
|
-
function
|
|
359
|
+
async function loginCommand() {
|
|
360
|
+
const creds = await authenticateTerminal();
|
|
361
|
+
process.stdout.write(`PreMan account ready.
|
|
362
|
+
|
|
363
|
+
Email: ${creds.user_email || "unknown"}
|
|
364
|
+
Backend: ${creds.backend_url}
|
|
365
|
+
API key: ${creds.api_key}
|
|
366
|
+
Saved to: ${CREDENTIALS_FILE}
|
|
367
|
+
|
|
368
|
+
You can now run:
|
|
369
|
+
npm exec -y premanmcp@latest -- install
|
|
370
|
+
`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function installCursorMcp() {
|
|
93
374
|
const serverName = argValue("--name", "preman");
|
|
94
375
|
const projectInstall = hasFlag("--project");
|
|
376
|
+
|
|
377
|
+
if (
|
|
378
|
+
!hasFlag("--print") &&
|
|
379
|
+
!hasFlag("--skip-login") &&
|
|
380
|
+
!argValue("--api-key", "") &&
|
|
381
|
+
!process.env.PREMAN_API_KEY &&
|
|
382
|
+
!readStoredCredentials()
|
|
383
|
+
) {
|
|
384
|
+
process.stdout.write("First, let's create or connect your PreMan account.\n");
|
|
385
|
+
await authenticateTerminal();
|
|
386
|
+
process.stdout.write("\n");
|
|
387
|
+
}
|
|
388
|
+
|
|
95
389
|
const serverConfig = buildServerConfig();
|
|
96
390
|
|
|
97
391
|
if (hasFlag("--print")) {
|
|
@@ -115,16 +409,18 @@ function installCursorMcp() {
|
|
|
115
409
|
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
116
410
|
writeFileSync(configPath, rendered, { mode: 0o600 });
|
|
117
411
|
|
|
118
|
-
const
|
|
412
|
+
const hasInlineKey = Boolean(config.mcpServers[serverName].env.PREMAN_API_KEY);
|
|
413
|
+
const hasStoredKey = Boolean(readStoredCredentials());
|
|
119
414
|
process.stdout.write(`PreMan MCP installed in ${configPath}
|
|
120
415
|
|
|
121
416
|
Server name: ${serverName}
|
|
122
417
|
Backend: ${config.mcpServers[serverName].env.PREMAN_BACKEND}
|
|
123
418
|
|
|
124
419
|
Next steps:
|
|
125
|
-
1. ${
|
|
420
|
+
1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : "Run npm exec -y premanmcp@latest -- login to create/connect your account and generate an API key."}
|
|
126
421
|
2. Restart Cursor or toggle the PreMan MCP server off/on.
|
|
127
|
-
3.
|
|
422
|
+
3. In your API repo, ask your coding agent:
|
|
423
|
+
"Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
|
|
128
424
|
`);
|
|
129
425
|
}
|
|
130
426
|
|
|
@@ -155,14 +451,30 @@ function startServer() {
|
|
|
155
451
|
});
|
|
156
452
|
}
|
|
157
453
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
454
|
+
async function main() {
|
|
455
|
+
if (command === "login") {
|
|
456
|
+
await loginCommand();
|
|
457
|
+
} else if (command === "install") {
|
|
458
|
+
await installCursorMcp();
|
|
459
|
+
} else if (command === "link") {
|
|
460
|
+
await linkCommand(commandArgs);
|
|
461
|
+
} else if (command === "tools") {
|
|
462
|
+
await toolsCommand(commandArgs);
|
|
463
|
+
} else if (command === "run") {
|
|
464
|
+
await runCommand(commandArgs);
|
|
465
|
+
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
466
|
+
printHelp();
|
|
467
|
+
} else if (command === "start") {
|
|
468
|
+
startServer();
|
|
469
|
+
} else {
|
|
470
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
471
|
+
printHelp();
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
168
474
|
}
|
|
475
|
+
|
|
476
|
+
main().catch((error) => {
|
|
477
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
478
|
+
process.stderr.write(`[preman] ${message}\n`);
|
|
479
|
+
process.exit(error?.exitCode || 1);
|
|
480
|
+
});
|