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 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
- npx premanmcp install
10
+ npm exec -y premanmcp@latest -- install
11
11
  ```
12
12
 
13
- This writes an `preman` MCP server into `~/.cursor/mcp.json`:
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": "npx",
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
- Replace `pm_live_YOUR_API_KEY_HERE` with an API key from [PreMan settings](https://app.preman.live/settings/), then restart Cursor or toggle the MCP server off/on.
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
- npx premanmcp install --api-key pm_live_xxx
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
- npx premanmcp install --project
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 the PreMan MCP to convert my login endpoint into an MCP tool.
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
- npx premanmcp install
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": "npx",
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 -- npx -y premanmcp
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
- npx premanmcp # Start the MCP server
108
- npx premanmcp install # Install Cursor MCP config
109
- npx premanmcp install --print # Print config without writing
110
- npx premanmcp install --project # Write .cursor/mcp.json in current repo
111
- npx premanmcp install --api-key KEY # Install with your PreMan API key
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
- * npx premanmcp install
8
- * npx premanmcp install --api-key pm_live_xxx
9
- * npx premanmcp
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 API_KEY_PLACEHOLDER = "pm_live_YOUR_API_KEY_HERE";
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
- npx premanmcp install [options] Install PreMan into Cursor MCP config
47
- npx premanmcp Start the PreMan MCP server
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. Defaults to ${API_KEY_PLACEHOLDER}
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
- npx premanmcp install
60
- npx premanmcp install --api-key pm_live_xxx
61
- npx premanmcp install --project --backend http://127.0.0.1:8000
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 || API_KEY_PLACEHOLDER);
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: "npx",
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 installCursorMcp() {
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 usedPlaceholder = config.mcpServers[serverName].env.PREMAN_API_KEY === API_KEY_PLACEHOLDER;
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. ${usedPlaceholder ? `Replace ${API_KEY_PLACEHOLDER} with your PreMan API key in ${configPath}. Find or create one at https://app.preman.live/settings/.` : "Your PreMan API key was written to the MCP config."}
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. Ask your agent: "Use the PreMan MCP to convert my login endpoint into an MCP tool." Then open https://app.preman.live/ to see the session and generated MCP.
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
- 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);
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
+ });