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