premanmcp 0.3.5 → 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 +38 -1
- package/bin/cli.js +51 -15
- package/bin/hosted.js +455 -0
- package/dist/server.js +110 -1
- package/dist/user_auth_flow.js +7 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -62,13 +62,14 @@ npm exec -y premanmcp@latest -- install --project
|
|
|
62
62
|
- Adds API-key auth for PreMan MCP access.
|
|
63
63
|
- Supports hosted MCPs with consumer tokens for customer-facing agent access.
|
|
64
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`).
|
|
65
66
|
|
|
66
67
|
## Common Agent Commands
|
|
67
68
|
|
|
68
69
|
After installing, ask your coding agent:
|
|
69
70
|
|
|
70
71
|
```text
|
|
71
|
-
Use
|
|
72
|
+
Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet.
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
```text
|
|
@@ -87,6 +88,10 @@ Convert these endpoints into an MCP.
|
|
|
87
88
|
Show me the audit log for this hosted MCP.
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
```text
|
|
92
|
+
Pull my pending PreMan fix tasks and fix the failing endpoint.
|
|
93
|
+
```
|
|
94
|
+
|
|
90
95
|
## Cursor
|
|
91
96
|
|
|
92
97
|
The installer targets Cursor by default:
|
|
@@ -135,11 +140,43 @@ Options:
|
|
|
135
140
|
- `--skip-login`: Install config without interactive terminal auth.
|
|
136
141
|
- `--print`: Print the generated MCP config without writing it.
|
|
137
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
|
+
|
|
138
172
|
## Environment Variables
|
|
139
173
|
|
|
140
174
|
- `PREMAN_API_KEY`: PreMan API key.
|
|
141
175
|
- `PREMAN_BACKEND`: PreMan backend URL.
|
|
142
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`.
|
|
143
180
|
|
|
144
181
|
If `PREMAN_API_KEY` is omitted, the MCP server loads credentials from `~/.preman/credentials.json`.
|
|
145
182
|
|
package/bin/cli.js
CHANGED
|
@@ -17,6 +17,8 @@ import path from "node:path";
|
|
|
17
17
|
import { createInterface } from "node:readline/promises";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
|
|
20
|
+
import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
|
|
21
|
+
|
|
20
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
23
|
const ROOT = path.join(__dirname, "..");
|
|
22
24
|
const DEFAULT_BACKEND = "https://api.preman.live";
|
|
@@ -49,6 +51,7 @@ Usage:
|
|
|
49
51
|
npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
|
|
50
52
|
npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
|
|
51
53
|
npm exec -y premanmcp@latest -- Start the PreMan MCP server
|
|
54
|
+
preman link|tools|run ... Drive a published hosted MCP
|
|
52
55
|
|
|
53
56
|
Login options:
|
|
54
57
|
--email <email> Pre-fill the email prompt
|
|
@@ -69,7 +72,7 @@ Examples:
|
|
|
69
72
|
npm exec -y premanmcp@latest -- install
|
|
70
73
|
npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
|
|
71
74
|
npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
|
|
72
|
-
`);
|
|
75
|
+
${HOSTED_HELP}`);
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function readJsonFile(filePath) {
|
|
@@ -224,6 +227,43 @@ async function verifyUnconfirmedAccount(email) {
|
|
|
224
227
|
return verified.access_token;
|
|
225
228
|
}
|
|
226
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
|
+
|
|
227
267
|
async function authenticateTerminal() {
|
|
228
268
|
const explicitKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
|
|
229
269
|
if (explicitKey && explicitKey.startsWith("pm_live_")) {
|
|
@@ -249,18 +289,7 @@ async function authenticateTerminal() {
|
|
|
249
289
|
|
|
250
290
|
if (!account.exists) {
|
|
251
291
|
process.stdout.write("No PreMan account found. Creating one now.\n");
|
|
252
|
-
|
|
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 || "");
|
|
292
|
+
accessToken = await createAccountFromTerminal(email);
|
|
264
293
|
} else if (account.needs_password) {
|
|
265
294
|
process.stdout.write("This account needs a password. Sending an OTP first.\n");
|
|
266
295
|
const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
|
|
@@ -390,7 +419,8 @@ Backend: ${config.mcpServers[serverName].env.PREMAN_BACKEND}
|
|
|
390
419
|
Next steps:
|
|
391
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."}
|
|
392
421
|
2. Restart Cursor or toggle the PreMan MCP server off/on.
|
|
393
|
-
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."
|
|
394
424
|
`);
|
|
395
425
|
}
|
|
396
426
|
|
|
@@ -426,6 +456,12 @@ async function main() {
|
|
|
426
456
|
await loginCommand();
|
|
427
457
|
} else if (command === "install") {
|
|
428
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);
|
|
429
465
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
430
466
|
printHelp();
|
|
431
467
|
} else if (command === "start") {
|
|
@@ -440,5 +476,5 @@ async function main() {
|
|
|
440
476
|
main().catch((error) => {
|
|
441
477
|
const message = error instanceof Error ? error.message : String(error);
|
|
442
478
|
process.stderr.write(`[preman] ${message}\n`);
|
|
443
|
-
process.exit(1);
|
|
479
|
+
process.exit(error?.exitCode || 1);
|
|
444
480
|
});
|
package/bin/hosted.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted MCP commands: link / tools / run.
|
|
3
|
+
*
|
|
4
|
+
* Drives a published PreMan hosted MCP from the terminal over the same surface
|
|
5
|
+
* agents use — POST /h/{id}/mcp, same selection, same consumer token, same
|
|
6
|
+
* audit trail. Nothing here talks to a PreMan control-plane API: this is
|
|
7
|
+
* packaging over the shipped runtime.
|
|
8
|
+
*
|
|
9
|
+
* Profiles live in ~/.preman/cli.json so `--env staging` picks a target. The
|
|
10
|
+
* runtime has no server-side notion of an environment; an env is a named
|
|
11
|
+
* profile pointing at one published hosted MCP.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
|
|
19
|
+
const PREMAN_DIR = path.join(os.homedir(), ".preman");
|
|
20
|
+
const CLI_CONFIG_FILE = path.join(PREMAN_DIR, "cli.json");
|
|
21
|
+
const DEFAULT_BACKEND = "https://api.preman.live";
|
|
22
|
+
const CONSUMER_TOKEN_PREFIX = "pm_hmcp_";
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
24
|
+
|
|
25
|
+
export const EXIT_TOOL_ERROR = 1;
|
|
26
|
+
export const EXIT_USAGE = 2;
|
|
27
|
+
export const EXIT_AUTH = 3;
|
|
28
|
+
export const EXIT_RPC = 4;
|
|
29
|
+
export const EXIT_NETWORK = 5;
|
|
30
|
+
|
|
31
|
+
export class CliError extends Error {
|
|
32
|
+
constructor(message, exitCode = EXIT_USAGE) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.exitCode = exitCode;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- arg parsing (mirrors bin/cli.js; no --flag=value form) -----------------
|
|
39
|
+
|
|
40
|
+
function makeArgs(commandArgs) {
|
|
41
|
+
return {
|
|
42
|
+
positional: commandArgs.filter((a, i) => {
|
|
43
|
+
if (a.startsWith("-")) return false;
|
|
44
|
+
const prev = commandArgs[i - 1];
|
|
45
|
+
return !(prev && prev.startsWith("-") && !FLAGS.has(prev));
|
|
46
|
+
}),
|
|
47
|
+
value(name, fallback = "") {
|
|
48
|
+
const index = commandArgs.indexOf(name);
|
|
49
|
+
if (index === -1) return fallback;
|
|
50
|
+
return commandArgs[index + 1] || fallback;
|
|
51
|
+
},
|
|
52
|
+
all(name) {
|
|
53
|
+
const out = [];
|
|
54
|
+
commandArgs.forEach((a, i) => {
|
|
55
|
+
if (a === name && commandArgs[i + 1]) out.push(commandArgs[i + 1]);
|
|
56
|
+
});
|
|
57
|
+
return out;
|
|
58
|
+
},
|
|
59
|
+
has(name) {
|
|
60
|
+
return commandArgs.includes(name);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Boolean flags take no value, so the token after them is still positional.
|
|
66
|
+
const FLAGS = new Set(["--default", "--no-verify", "--json-out", "--quiet"]);
|
|
67
|
+
|
|
68
|
+
// --- profile store ---------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export function readCliConfig() {
|
|
71
|
+
if (!existsSync(CLI_CONFIG_FILE)) return { version: 1, profiles: {} };
|
|
72
|
+
const raw = readFileSync(CLI_CONFIG_FILE, "utf8").trim();
|
|
73
|
+
if (!raw) return { version: 1, profiles: {} };
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(raw);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new CliError(`Could not parse ${CLI_CONFIG_FILE}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
if (!parsed.profiles || typeof parsed.profiles !== "object") parsed.profiles = {};
|
|
81
|
+
if (!parsed.version) parsed.version = 1;
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeCliConfig(config) {
|
|
86
|
+
mkdirSync(PREMAN_DIR, { recursive: true, mode: 0o700 });
|
|
87
|
+
writeFileSync(CLI_CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function maskToken(token) {
|
|
91
|
+
if (!token) return "(none)";
|
|
92
|
+
return `${CONSUMER_TOKEN_PREFIX}…${String(token).slice(-4)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Accept a full /h/<id>/mcp URL or a bare hosted MCP id. */
|
|
96
|
+
export function resolveMcpUrl(target, backend) {
|
|
97
|
+
const value = String(target || "").trim();
|
|
98
|
+
if (!value) throw new CliError("Missing hosted MCP URL or id.");
|
|
99
|
+
if (/^https?:\/\//i.test(value)) {
|
|
100
|
+
return value.replace(/\/+$/, "");
|
|
101
|
+
}
|
|
102
|
+
if (value.includes("/")) {
|
|
103
|
+
throw new CliError(`Not a hosted MCP id or URL: ${value}`);
|
|
104
|
+
}
|
|
105
|
+
const base = (backend || process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
|
|
106
|
+
return `${base}/h/${value}/mcp`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pick the target profile.
|
|
111
|
+
*
|
|
112
|
+
* --env wins, then PREMAN_MCP_URL (so CI needs no config file), then the
|
|
113
|
+
* default profile, then a lone profile. Anything else is ambiguous.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveProfile(config, envName, { toolPrefix = "" } = {}) {
|
|
116
|
+
const profiles = config.profiles || {};
|
|
117
|
+
const names = Object.keys(profiles);
|
|
118
|
+
|
|
119
|
+
if (envName && toolPrefix) {
|
|
120
|
+
throw new CliError(
|
|
121
|
+
`Ambiguous target: --env ${envName} together with the dotted form '${toolPrefix}.…'. Use one.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const wanted = envName || toolPrefix;
|
|
126
|
+
if (wanted) {
|
|
127
|
+
if (profiles[wanted]) return { name: wanted, ...profiles[wanted] };
|
|
128
|
+
const byAlias = names.find((n) => profiles[n].alias === wanted);
|
|
129
|
+
if (byAlias) return { name: byAlias, ...profiles[byAlias] };
|
|
130
|
+
const known = names.length ? names.join(", ") : "none";
|
|
131
|
+
throw new CliError(`No profile named '${wanted}'. Linked profiles: ${known}.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (process.env.PREMAN_MCP_URL) {
|
|
135
|
+
return {
|
|
136
|
+
name: "(env)",
|
|
137
|
+
mcp_url: process.env.PREMAN_MCP_URL.replace(/\/+$/, ""),
|
|
138
|
+
token: process.env.PREMAN_MCP_TOKEN || "",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const preferred = config.default_profile;
|
|
143
|
+
if (preferred && profiles[preferred]) return { name: preferred, ...profiles[preferred] };
|
|
144
|
+
if (names.length === 1) return { name: names[0], ...profiles[names[0]] };
|
|
145
|
+
|
|
146
|
+
if (!names.length) {
|
|
147
|
+
throw new CliError(
|
|
148
|
+
"No hosted MCP linked. Run: preman link <mcp-url-or-id> --env <name> [--token pm_hmcp_…]",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
throw new CliError(`Multiple profiles linked (${names.join(", ")}). Pass --env <name>.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- JSON-RPC over the hosted runtime --------------------------------------
|
|
155
|
+
|
|
156
|
+
let cachedVersion = null;
|
|
157
|
+
function cliVersion() {
|
|
158
|
+
if (cachedVersion) return cachedVersion;
|
|
159
|
+
try {
|
|
160
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
161
|
+
cachedVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0";
|
|
162
|
+
} catch {
|
|
163
|
+
cachedVersion = "0.0.0";
|
|
164
|
+
}
|
|
165
|
+
return cachedVersion;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The runtime answers HTTP 200 even for errors — the JSON-RPC envelope is the
|
|
170
|
+
* only source of truth, so never branch on resp.status here.
|
|
171
|
+
*/
|
|
172
|
+
export async function mcpRpc(profile, method, params, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
173
|
+
const headers = {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
Accept: "application/json, text/event-stream",
|
|
176
|
+
};
|
|
177
|
+
if (profile.token) headers.Authorization = `Bearer ${profile.token}`;
|
|
178
|
+
|
|
179
|
+
let resp;
|
|
180
|
+
try {
|
|
181
|
+
resp = await fetch(profile.mcp_url, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers,
|
|
184
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
185
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const reason = error?.name === "TimeoutError" ? "timed out" : error.message;
|
|
189
|
+
throw new CliError(`Could not reach ${profile.mcp_url}: ${reason}`, EXIT_NETWORK);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const text = await resp.text();
|
|
193
|
+
let envelope;
|
|
194
|
+
try {
|
|
195
|
+
envelope = JSON.parse(text);
|
|
196
|
+
} catch {
|
|
197
|
+
throw new CliError(
|
|
198
|
+
`Unexpected non-JSON response from ${profile.mcp_url} (HTTP ${resp.status}).`,
|
|
199
|
+
EXIT_NETWORK,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (envelope.error) {
|
|
204
|
+
const { code, message } = envelope.error;
|
|
205
|
+
if (code === -32001) {
|
|
206
|
+
throw new CliError(
|
|
207
|
+
`Authentication failed for profile '${profile.name}'. ` +
|
|
208
|
+
"Re-link with a valid consumer token: preman link <url> --env <name> --token pm_hmcp_…",
|
|
209
|
+
EXIT_AUTH,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (code === -32601 && method === "tools/call") {
|
|
213
|
+
throw new CliError(
|
|
214
|
+
`${message || "Unknown tool"}. Run 'preman tools${
|
|
215
|
+
profile.name && profile.name !== "(env)" ? ` --env ${profile.name}` : ""
|
|
216
|
+
}' to list available tools.`,
|
|
217
|
+
EXIT_RPC,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
throw new CliError(message || `JSON-RPC error ${code}`, EXIT_RPC);
|
|
221
|
+
}
|
|
222
|
+
return envelope.result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function initialize(profile, timeoutMs) {
|
|
226
|
+
// Sending clientInfo lets the runtime attribute invocations to the CLI.
|
|
227
|
+
return mcpRpc(
|
|
228
|
+
profile,
|
|
229
|
+
"initialize",
|
|
230
|
+
{
|
|
231
|
+
protocolVersion: "2024-11-05",
|
|
232
|
+
capabilities: {},
|
|
233
|
+
clientInfo: { name: "preman-cli", version: cliVersion() },
|
|
234
|
+
},
|
|
235
|
+
{ timeoutMs },
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- argument assembly -----------------------------------------------------
|
|
240
|
+
|
|
241
|
+
async function readStdin() {
|
|
242
|
+
const chunks = [];
|
|
243
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
244
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** `key=value` is a string; `key:=value` is parsed as JSON. */
|
|
248
|
+
export function parseArgPairs(pairs, base = {}) {
|
|
249
|
+
const out = { ...base };
|
|
250
|
+
for (const pair of pairs) {
|
|
251
|
+
const jsonIndex = pair.indexOf(":=");
|
|
252
|
+
const eqIndex = pair.indexOf("=");
|
|
253
|
+
if (jsonIndex !== -1 && (eqIndex === -1 || jsonIndex < eqIndex)) {
|
|
254
|
+
const key = pair.slice(0, jsonIndex);
|
|
255
|
+
const raw = pair.slice(jsonIndex + 2);
|
|
256
|
+
try {
|
|
257
|
+
out[key] = JSON.parse(raw);
|
|
258
|
+
} catch {
|
|
259
|
+
throw new CliError(`--arg ${key}:= expects JSON, got ${raw === "" ? "(empty)" : raw}`);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (eqIndex === -1) {
|
|
264
|
+
throw new CliError(`--arg expects key=value or key:=json, got '${pair}'`);
|
|
265
|
+
}
|
|
266
|
+
out[pair.slice(0, eqIndex)] = pair.slice(eqIndex + 1);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function buildToolArguments(args) {
|
|
272
|
+
let base = {};
|
|
273
|
+
const jsonArg = args.value("--json", "");
|
|
274
|
+
if (jsonArg) {
|
|
275
|
+
const raw = jsonArg === "-" ? await readStdin() : jsonArg;
|
|
276
|
+
try {
|
|
277
|
+
base = JSON.parse(raw);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new CliError(`--json expects a JSON object: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
if (base === null || typeof base !== "object" || Array.isArray(base)) {
|
|
282
|
+
throw new CliError("--json must be a JSON object.");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return parseArgPairs(args.all("--arg"), base);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function promptToken() {
|
|
289
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
290
|
+
try {
|
|
291
|
+
return (await rl.question("Consumer token (pm_hmcp_…, blank for public MCPs): ")).trim();
|
|
292
|
+
} finally {
|
|
293
|
+
rl.close();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- commands --------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
export async function linkCommand(commandArgs) {
|
|
300
|
+
const args = makeArgs(commandArgs);
|
|
301
|
+
const target = args.positional[0];
|
|
302
|
+
const envName = args.value("--env", "");
|
|
303
|
+
if (!target) throw new CliError("Usage: preman link <mcp-url-or-id> --env <name> [--token pm_hmcp_…]");
|
|
304
|
+
if (!envName) throw new CliError("preman link requires --env <name> to save the profile under.");
|
|
305
|
+
|
|
306
|
+
const mcpUrl = resolveMcpUrl(target, args.value("--backend", ""));
|
|
307
|
+
let token = args.value("--token", "") || process.env.PREMAN_MCP_TOKEN || "";
|
|
308
|
+
if (!token && process.stdin.isTTY) token = await promptToken();
|
|
309
|
+
|
|
310
|
+
if (token && !token.startsWith(CONSUMER_TOKEN_PREFIX)) {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`[preman] Warning: consumer tokens normally start with ${CONSUMER_TOKEN_PREFIX}.\n`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const profile = { name: envName, mcp_url: mcpUrl, token };
|
|
317
|
+
let alias = args.value("--alias", "");
|
|
318
|
+
let toolCount = null;
|
|
319
|
+
|
|
320
|
+
if (!args.has("--no-verify")) {
|
|
321
|
+
const info = await initialize(profile);
|
|
322
|
+
if (!alias) alias = info?.serverInfo?.name || "";
|
|
323
|
+
const listed = await mcpRpc(profile, "tools/list", {});
|
|
324
|
+
toolCount = (listed?.tools || []).length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const config = readCliConfig();
|
|
328
|
+
config.profiles[envName] = {
|
|
329
|
+
mcp_url: mcpUrl,
|
|
330
|
+
...(token ? { token } : {}),
|
|
331
|
+
...(alias ? { alias } : {}),
|
|
332
|
+
linked_at: new Date().toISOString(),
|
|
333
|
+
};
|
|
334
|
+
if (args.has("--default") || !config.default_profile) config.default_profile = envName;
|
|
335
|
+
writeCliConfig(config);
|
|
336
|
+
|
|
337
|
+
process.stdout.write(
|
|
338
|
+
`Linked '${envName}' → ${mcpUrl}\n` +
|
|
339
|
+
` token: ${token ? maskToken(token) : "(none — public access)"}\n` +
|
|
340
|
+
(toolCount === null ? " (verification skipped)\n" : ` tools available: ${toolCount}\n`) +
|
|
341
|
+
` saved to ${CLI_CONFIG_FILE}\n`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function toolsCommand(commandArgs) {
|
|
346
|
+
const args = makeArgs(commandArgs);
|
|
347
|
+
const config = readCliConfig();
|
|
348
|
+
const profile = resolveProfile(config, args.value("--env", ""));
|
|
349
|
+
const timeoutMs = Number(args.value("--timeout", "")) * 1000 || DEFAULT_TIMEOUT_MS;
|
|
350
|
+
|
|
351
|
+
const result = await mcpRpc(profile, "tools/list", {}, { timeoutMs });
|
|
352
|
+
const tools = result?.tools || [];
|
|
353
|
+
if (args.has("--json-out")) {
|
|
354
|
+
process.stdout.write(`${JSON.stringify(tools, null, 2)}\n`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!tools.length) {
|
|
358
|
+
process.stdout.write(`No tools published on '${profile.name}'.\n`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write(`${tools.length} tool(s) on '${profile.name}':\n`);
|
|
362
|
+
for (const tool of tools) {
|
|
363
|
+
const required = tool.inputSchema?.required || [];
|
|
364
|
+
const suffix = required.length ? ` (required: ${required.join(", ")})` : "";
|
|
365
|
+
process.stdout.write(` ${tool.name}${suffix}\n`);
|
|
366
|
+
if (tool.description) process.stdout.write(` ${tool.description}\n`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function runCommand(commandArgs) {
|
|
371
|
+
const args = makeArgs(commandArgs);
|
|
372
|
+
const selection = args.positional[0];
|
|
373
|
+
if (!selection) {
|
|
374
|
+
throw new CliError(
|
|
375
|
+
"Usage: preman run <tool> [--env <name>] [--arg key=value] [--json '{...}']",
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Dotted form addresses a profile explicitly: <profile-or-alias>.<tool>
|
|
380
|
+
const dot = selection.indexOf(".");
|
|
381
|
+
const toolPrefix = dot > 0 ? selection.slice(0, dot) : "";
|
|
382
|
+
const toolName = dot > 0 ? selection.slice(dot + 1) : selection;
|
|
383
|
+
if (dot > 0 && !toolName) throw new CliError(`Missing tool name after '${toolPrefix}.'`);
|
|
384
|
+
|
|
385
|
+
const config = readCliConfig();
|
|
386
|
+
const profile = resolveProfile(config, args.value("--env", ""), { toolPrefix });
|
|
387
|
+
const toolArguments = await buildToolArguments(args);
|
|
388
|
+
const timeoutMs = Number(args.value("--timeout", "")) * 1000 || DEFAULT_TIMEOUT_MS;
|
|
389
|
+
|
|
390
|
+
const result = await mcpRpc(
|
|
391
|
+
profile,
|
|
392
|
+
"tools/call",
|
|
393
|
+
{ name: toolName, arguments: toolArguments },
|
|
394
|
+
{ timeoutMs },
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (args.has("--json-out")) {
|
|
398
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
399
|
+
} else {
|
|
400
|
+
for (const block of result?.content || []) {
|
|
401
|
+
if (typeof block?.text === "string") process.stdout.write(`${block.text}\n`);
|
|
402
|
+
}
|
|
403
|
+
const status = result?._meta?.status;
|
|
404
|
+
if (status !== undefined && !args.has("--quiet")) {
|
|
405
|
+
process.stderr.write(`[preman] upstream status ${status}\n`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (result?.isError) {
|
|
410
|
+
// The runtime reports an unmatched tool name as an isError result rather
|
|
411
|
+
// than a JSON-RPC error, so point at the discovery command from here too.
|
|
412
|
+
const text = (result.content || []).map((b) => b?.text || "").join(" ");
|
|
413
|
+
if (/unknown tool/i.test(text)) {
|
|
414
|
+
process.stderr.write(
|
|
415
|
+
`[preman] Run 'preman tools${
|
|
416
|
+
profile.name && profile.name !== "(env)" ? ` --env ${profile.name}` : ""
|
|
417
|
+
}' to list available tools.\n`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
process.exitCode = EXIT_TOOL_ERROR;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const HOSTED_HELP = `
|
|
425
|
+
Hosted MCP (drive a published selection from the terminal):
|
|
426
|
+
preman link <mcp-url-or-id> --env <name> Save a hosted MCP profile and verify it
|
|
427
|
+
preman tools [--env <name>] List callable tools on the linked MCP
|
|
428
|
+
preman run <tool> [--env <name>] [args] Call a tool and print the result
|
|
429
|
+
|
|
430
|
+
Link options:
|
|
431
|
+
--env <name> Profile name to save under (required)
|
|
432
|
+
--token <pm_hmcp_…> Consumer token (or PREMAN_MCP_TOKEN, or prompted; omit for public MCPs)
|
|
433
|
+
--backend <url> Backend URL when passing a bare MCP id. Defaults to ${DEFAULT_BACKEND}
|
|
434
|
+
--alias <name> Short name usable in the dotted form
|
|
435
|
+
--default Make this the default profile
|
|
436
|
+
--no-verify Skip the initialize + tools/list check
|
|
437
|
+
|
|
438
|
+
Run options:
|
|
439
|
+
--env <name> Profile to use. Falls back to PREMAN_MCP_URL, then the default profile
|
|
440
|
+
--arg key=value String argument (repeatable)
|
|
441
|
+
--arg key:=json JSON-typed argument, e.g. --arg qty:=2 (repeatable)
|
|
442
|
+
--json '{...}' Full arguments object ('--json -' reads stdin)
|
|
443
|
+
--timeout <seconds> Client-side timeout. Defaults to 60
|
|
444
|
+
--json-out Print the raw JSON-RPC result
|
|
445
|
+
<tool> may be dotted: <profile-or-alias>.<tool_name>
|
|
446
|
+
|
|
447
|
+
Hosted MCP environment:
|
|
448
|
+
PREMAN_MCP_URL Hosted MCP endpoint (…/h/<id>/mcp); used when --env is omitted
|
|
449
|
+
PREMAN_MCP_TOKEN Consumer token for PREMAN_MCP_URL
|
|
450
|
+
|
|
451
|
+
Examples:
|
|
452
|
+
preman link https://api.preman.live/h/2f6c…/mcp --env staging --token pm_hmcp_xxx
|
|
453
|
+
preman tools --env staging
|
|
454
|
+
preman run post_users_id_orders --env staging --arg id=42 --json '{"body":{"sku":"A1"}}'
|
|
455
|
+
`;
|
package/dist/server.js
CHANGED
|
@@ -42,6 +42,62 @@ applyRepoPremanConfig();
|
|
|
42
42
|
const BACKEND_URL = process.env.PREMAN_BACKEND || "https://api.preman.live";
|
|
43
43
|
const FRONTEND_URL = process.env.PREMAN_FRONTEND || "https://app.preman.live";
|
|
44
44
|
let API_KEY = process.env.PREMAN_API_KEY || "";
|
|
45
|
+
function detectCodingAgent() {
|
|
46
|
+
const forced = (process.env.PREMAN_CODING_AGENT || "").trim().toLowerCase();
|
|
47
|
+
if (forced)
|
|
48
|
+
return forced.replace("-", "_");
|
|
49
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE)
|
|
50
|
+
return "claude_code";
|
|
51
|
+
if (process.env.CODEX_HOME || process.env.OPENAI_CODEX)
|
|
52
|
+
return "codex";
|
|
53
|
+
if (process.env.CURSOR_AGENT || process.env.CURSOR_TRACE_ID || process.env.CURSOR_SESSION_ID) {
|
|
54
|
+
return "cursor";
|
|
55
|
+
}
|
|
56
|
+
return "cursor";
|
|
57
|
+
}
|
|
58
|
+
/** Prove this MCP session is live to the workbench coding-agent link. */
|
|
59
|
+
async function heartbeatWorkbenchLink() {
|
|
60
|
+
if (!API_KEY)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
const resp = await fetch(`${BACKEND_URL}/workbench/coding-agent/heartbeat`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
pair_code: process.env.PREMAN_PAIR_CODE || undefined,
|
|
71
|
+
agent: detectCodingAgent(),
|
|
72
|
+
project_path: process.cwd(),
|
|
73
|
+
client_label: "premanmcp",
|
|
74
|
+
source: "preman_status",
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const text = await resp.text();
|
|
78
|
+
let data = {};
|
|
79
|
+
try {
|
|
80
|
+
data = text ? JSON.parse(text) : {};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
data = { detail: text };
|
|
84
|
+
}
|
|
85
|
+
if (!resp.ok) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
status: resp.status,
|
|
89
|
+
detail: data.detail || data.message || text,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, ...data };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
45
101
|
const PREMAN_CONTROL_PLANE_HOSTS = new Set([
|
|
46
102
|
"api.preman.live",
|
|
47
103
|
"preman.live",
|
|
@@ -920,6 +976,7 @@ export function createServer() {
|
|
|
920
976
|
}],
|
|
921
977
|
};
|
|
922
978
|
}
|
|
979
|
+
const workbench = await heartbeatWorkbenchLink();
|
|
923
980
|
return {
|
|
924
981
|
content: [{
|
|
925
982
|
type: "text",
|
|
@@ -930,13 +987,17 @@ export function createServer() {
|
|
|
930
987
|
backend_url: BACKEND_URL,
|
|
931
988
|
frontend_base_url: FRONTEND_BASE,
|
|
932
989
|
endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
|
|
990
|
+
coding_agent: workbench,
|
|
933
991
|
_agent_hints: {
|
|
934
992
|
next_actions: [
|
|
993
|
+
workbench && workbench.connected
|
|
994
|
+
? "Coding agent is linked to PreMan workbench — call discover_endpoints_from_codebase or get_endpoints."
|
|
995
|
+
: "If PreMan workbench shows pairing, set PREMAN_PAIR_CODE then call preman_status again.",
|
|
935
996
|
"browser_navigate to `endpoints_page_url` (SPA shell + ot_agent_route) to open PreMan on the Endpoints page (sign in if prompted), then call get_endpoints.",
|
|
936
997
|
"Call get_endpoints to see registered API endpoints.",
|
|
937
998
|
"Call test_api to test an endpoint.",
|
|
938
999
|
],
|
|
939
|
-
related_tools: ["get_endpoints", "test_api", "import_collection"],
|
|
1000
|
+
related_tools: ["get_endpoints", "test_api", "import_collection", "discover_endpoints_from_codebase"],
|
|
940
1001
|
},
|
|
941
1002
|
}),
|
|
942
1003
|
}],
|
|
@@ -1206,6 +1267,54 @@ export function createServer() {
|
|
|
1206
1267
|
return toolError(e.message, inferErrorCode(e.message));
|
|
1207
1268
|
}
|
|
1208
1269
|
});
|
|
1270
|
+
// ── preman_get_fix_task ───────────────────────────────────────────
|
|
1271
|
+
server.tool("preman_get_fix_task", "Pull pending coding-agent fix tasks built from fired PreMan alerts. Each task packages a failing endpoint: title, expected vs actual, failure stats, and a reproducible curl (package.repro.curl). Fix the endpoint using the curl, then call preman_complete_fix_task with the fix_task_id. Check package.auto_pr: when eligible is true, push your fix on the branch it names and call preman_open_fix_pr.", {
|
|
1272
|
+
status: z.enum(["open", "delivered", "resolved"]).optional().default("open").describe("'open' (default) hands out new tasks and marks them delivered; 'delivered' re-fetches ones already pulled; 'resolved' for history"),
|
|
1273
|
+
limit: z.number().optional().default(5).describe("Max tasks to return (capped at 20)"),
|
|
1274
|
+
}, async (args) => {
|
|
1275
|
+
try {
|
|
1276
|
+
const result = await callBackend("preman_get_fix_task", args);
|
|
1277
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1278
|
+
}
|
|
1279
|
+
catch (e) {
|
|
1280
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1281
|
+
next_actions: ["No open fix tasks means no alerts have been handed off. Create a handoff from a fired alert in the dashboard."],
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
// ── preman_complete_fix_task ──────────────────────────────────────
|
|
1286
|
+
server.tool("preman_complete_fix_task", "Mark a fix task resolved once its endpoint failure is fixed.", {
|
|
1287
|
+
fix_task_id: z.string().describe("The id from a preman_get_fix_task result"),
|
|
1288
|
+
resolution_note: z.string().optional().default("").describe("Optional note on what was fixed"),
|
|
1289
|
+
}, async (args) => {
|
|
1290
|
+
try {
|
|
1291
|
+
const result = await callBackend("preman_complete_fix_task", args);
|
|
1292
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1293
|
+
}
|
|
1294
|
+
catch (e) {
|
|
1295
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1296
|
+
related_tools: ["preman_get_fix_task"],
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
// ── preman_open_fix_pr ────────────────────────────────────────────
|
|
1301
|
+
server.tool("preman_open_fix_pr", "Open a pull request for a fix branch you already pushed (Auto-PR, Tier 2). Only for fix tasks whose package.auto_pr.eligible is true. Patch and push the preman/fix-* branch with your own git credentials first — PreMan never pushes code. PreMan then verifies the branch exists, re-checks the endpoint in production, and opens a PR with that evidence. PreMan never merges: a human reviews and merges.", {
|
|
1302
|
+
fix_task_id: z.string().describe("The id from a preman_get_fix_task result"),
|
|
1303
|
+
branch: z.string().describe("The branch you pushed — must match package.auto_pr.branch"),
|
|
1304
|
+
summary: z.string().optional().default("").describe("Short description of the fix, included in the PR body"),
|
|
1305
|
+
local_rerun: z.string().optional().default("").describe("Your local test/re-run output, included as agent-reported evidence"),
|
|
1306
|
+
}, async (args) => {
|
|
1307
|
+
try {
|
|
1308
|
+
const result = await callBackend("preman_open_fix_pr", args);
|
|
1309
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1313
|
+
related_tools: ["preman_get_fix_task", "preman_complete_fix_task"],
|
|
1314
|
+
next_actions: ["Auto-PR requires the repo to have opted in and the fix task to be an API_BUG failure mapped to that repo."],
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1209
1318
|
return server;
|
|
1210
1319
|
}
|
|
1211
1320
|
// ── Start ─────────────────────────────────────────────────────────────
|
package/dist/user_auth_flow.js
CHANGED
|
@@ -58,7 +58,7 @@ async function callAuthJson(base, method, path, opts) {
|
|
|
58
58
|
*/
|
|
59
59
|
export function registerUserAuthFlowTools(server, backendUrl) {
|
|
60
60
|
const base = normalizeBase(backendUrl);
|
|
61
|
-
server.tool("user_auth_start_signup", "Start signup with email only. Sends an OTP; next call user_auth_set_password with email, OTP, and new password. Uses POST /auth/start-signup on PREMAN_BACKEND (no API key).", {
|
|
61
|
+
server.tool("user_auth_start_signup", "Start signup with email only. Sends an OTP; next call user_auth_set_password with email, OTP, and new password. Uses POST /auth/start-signup on PREMAN_BACKEND (no API key). If the backend does not support this endpoint yet, use user_auth_signup instead.", {
|
|
62
62
|
email: z.string().describe("User email"),
|
|
63
63
|
}, async (args) => {
|
|
64
64
|
try {
|
|
@@ -66,6 +66,12 @@ export function registerUserAuthFlowTools(server, backendUrl) {
|
|
|
66
66
|
json: { email: args.email },
|
|
67
67
|
});
|
|
68
68
|
if (!r.ok) {
|
|
69
|
+
if (r.status_code === 404) {
|
|
70
|
+
return toolError("This backend does not support email-only signup yet. Use user_auth_signup with email and password, then user_auth_verify_otp.", "backend_error", {
|
|
71
|
+
next_actions: ["Call user_auth_signup with email and password.", "Then call user_auth_verify_otp with the email code."],
|
|
72
|
+
related_tools: ["user_auth_signup", "user_auth_verify_otp"],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
69
75
|
return toolError(String(r.detail ?? r.message ?? "start signup failed"), r.status_code === 400 ? "invalid_input" : "backend_error", {
|
|
70
76
|
next_actions: ["If email exists, use user_auth_login or user_auth_resend_otp."],
|
|
71
77
|
related_tools: ["user_auth_set_password", "user_auth_login"],
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"premanmcp": "bin/cli.js",
|
|
8
|
-
"preman-mcp": "bin/cli.js"
|
|
8
|
+
"preman-mcp": "bin/cli.js",
|
|
9
|
+
"preman": "bin/cli.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc -p tsconfig.server.json",
|