premanmcp 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -21
- package/bin/cli.js +345 -33
- package/bin/hosted.js +455 -0
- package/dist/auth_flow_ui.d.ts +3 -0
- package/dist/auth_flow_ui.js +13 -2
- package/dist/server.js +219 -22
- package/dist/user_auth_flow.js +28 -0
- package/package.json +3 -2
package/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/auth_flow_ui.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type ShareAuthFlowResult = {
|
|
|
18
18
|
session_id: string;
|
|
19
19
|
url: string;
|
|
20
20
|
endpoint_count: number;
|
|
21
|
+
user_id: number | null;
|
|
22
|
+
auto_discoverable: boolean;
|
|
21
23
|
upstream_base_url: string;
|
|
22
24
|
endpoints: AuthFlowEndpoint[];
|
|
23
25
|
ui: {
|
|
@@ -32,4 +34,5 @@ export declare function shareAuthFlowToUi(opts: {
|
|
|
32
34
|
upstreamBaseUrl?: string;
|
|
33
35
|
sessionId?: string;
|
|
34
36
|
intent?: string;
|
|
37
|
+
apiKey?: string;
|
|
35
38
|
}): Promise<ShareAuthFlowResult>;
|
package/dist/auth_flow_ui.js
CHANGED
|
@@ -110,6 +110,10 @@ export function buildAuthFlowEndpoints() {
|
|
|
110
110
|
export async function shareAuthFlowToUi(opts) {
|
|
111
111
|
const backend = opts.backendUrl.replace(/\/+$/, "");
|
|
112
112
|
const frontend = opts.frontendUrl.replace(/\/+$/, "");
|
|
113
|
+
const apiKey = opts.apiKey?.trim();
|
|
114
|
+
if (!apiKey) {
|
|
115
|
+
throw new Error("PreMan authentication required. Run preman_login first so auth-flow sessions can stream to your dashboard.");
|
|
116
|
+
}
|
|
113
117
|
const upstream = (opts.upstreamBaseUrl || backend).replace(/\/+$/, "") || backend;
|
|
114
118
|
const sessionId = opts.sessionId?.trim() || crypto.randomUUID();
|
|
115
119
|
const endpoints = buildAuthFlowEndpoints().map((ep) => ({
|
|
@@ -118,11 +122,16 @@ export async function shareAuthFlowToUi(opts) {
|
|
|
118
122
|
}));
|
|
119
123
|
const resp = await fetch(`${backend}/agent-sessions/${encodeURIComponent(sessionId)}/endpoints`, {
|
|
120
124
|
method: "POST",
|
|
121
|
-
headers: {
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Accept: "application/json",
|
|
128
|
+
Authorization: `Bearer ${apiKey}`,
|
|
129
|
+
},
|
|
122
130
|
body: JSON.stringify({
|
|
123
131
|
endpoints,
|
|
124
132
|
upstream_base_url: upstream,
|
|
125
133
|
intent: opts.intent || "Auth flow: signup, verify OTP, login, resend OTP",
|
|
134
|
+
client_label: "premanmcp",
|
|
126
135
|
}),
|
|
127
136
|
});
|
|
128
137
|
const text = await resp.text();
|
|
@@ -142,11 +151,13 @@ export async function shareAuthFlowToUi(opts) {
|
|
|
142
151
|
session_id: sid,
|
|
143
152
|
url,
|
|
144
153
|
endpoint_count: Number(body.endpoint_count ?? endpoints.length),
|
|
154
|
+
user_id: typeof body.user_id === "number" ? body.user_id : null,
|
|
155
|
+
auto_discoverable: Boolean(body.auto_discoverable),
|
|
145
156
|
upstream_base_url: upstream,
|
|
146
157
|
endpoints: buildAuthFlowEndpoints(),
|
|
147
158
|
ui: {
|
|
148
159
|
url,
|
|
149
|
-
note: "Open in Cursor Agent Browser. Test signup → verify-otp → login, or resend-otp. " +
|
|
160
|
+
note: "Open in Cursor Agent Browser or the Playground session list. Test signup → verify-otp → login, or resend-otp. " +
|
|
150
161
|
"Schemas are prefilled from routes/auth Pydantic models.",
|
|
151
162
|
},
|
|
152
163
|
related_tools: [
|