premanmcp 0.11.0 → 0.13.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 +25 -7
- package/bin/cli.js +10 -9
- package/bin/desktop.js +119 -9
- package/bin/integrations.js +31 -44
- package/dist/server.js +84 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -4,14 +4,24 @@ 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
|
+
## Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm exec -y premanmcp@latest -- onboard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Create the account, verify the email, and get the PreMan app installed and opened
|
|
14
|
+
already signed in. A bare `preman` in a terminal does the same thing.
|
|
15
|
+
|
|
16
|
+
## Connect a coding agent
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
19
|
npm exec -y premanmcp@latest -- connect
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
Pick your coding agent from the list — Cursor,
|
|
14
|
-
writes that agent's MCP config for you. No hand-edited
|
|
22
|
+
Optional, and separate from starting out. Pick your coding agent from the list — Cursor,
|
|
23
|
+
Claude Code, or Codex — and PreMan writes that agent's MCP config for you. No hand-edited
|
|
24
|
+
config anywhere.
|
|
15
25
|
|
|
16
26
|
Local development form:
|
|
17
27
|
|
|
@@ -82,10 +92,18 @@ preman github # or connect it in the dashboard
|
|
|
82
92
|
preman status # which of those are done
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
`preman onboard` (or `setup
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
`preman onboard` (or `setup`, or a bare `preman` in a terminal) is what someone starting
|
|
96
|
+
out runs: create the account and verify the email, then install the PreMan app and open it
|
|
97
|
+
already signed in, then GitHub, AWS, Slack — one question per step, `b` to go back, and a
|
|
98
|
+
summary at the end. Connecting a coding agent is no longer part of starting out; run
|
|
99
|
+
`preman connect` when you actually want one wired into an IDE. `connect --guide` still runs
|
|
100
|
+
the full pass inside connect itself: discovery, a first test, the runner, the desktop app
|
|
101
|
+
and the integration prompts.
|
|
102
|
+
|
|
103
|
+
The app opens signed in because the CLI leaves the key it just minted in
|
|
104
|
+
`~/.preman/desktop-session.json`, which the app reads once and deletes. An app too old to
|
|
105
|
+
look for it, or a machine that is not macOS, falls back to signing in on the app's own
|
|
106
|
+
login screen with the account you just created.
|
|
89
107
|
|
|
90
108
|
Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
|
|
91
109
|
project-local config, `--print` shows the config without writing it, `--no-hook` leaves
|
package/bin/cli.js
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
CONNECT_HELP,
|
|
23
23
|
DISPATCH_HELP,
|
|
24
24
|
connectCommand,
|
|
25
|
-
discoverEndpoints,
|
|
26
25
|
dispatchCommand,
|
|
27
26
|
resolveAgentForPairing,
|
|
28
27
|
writeCursorConfig,
|
|
@@ -39,7 +38,7 @@ import { STATUS_HELP, statusCommand } from "./status.js";
|
|
|
39
38
|
import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
|
|
40
39
|
import { RUNNER_HELP, runnerCommand } from "./runner.js";
|
|
41
40
|
import { VERIFY_HELP, verifyCommand } from "./verify.js";
|
|
42
|
-
import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
|
|
41
|
+
import { DESKTOP_HELP, installDesktopCommand, openDesktopSignedIn } from "./desktop.js";
|
|
43
42
|
import { ACCOUNT_HELP, doctorCommand, loginBrowser, logoutCommand, watchCommand } from "./account.js";
|
|
44
43
|
import {
|
|
45
44
|
CREDENTIALS_FILE,
|
|
@@ -57,8 +56,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
57
56
|
const ROOT = path.join(__dirname, "..");
|
|
58
57
|
|
|
59
58
|
const args = process.argv.slice(2);
|
|
59
|
+
// A bare `preman` in a terminal is someone starting out, so it runs setup:
|
|
60
|
+
// account first, then the app. Piped or redirected (no TTY) it is an MCP host
|
|
61
|
+
// launching the server, which must stay the behaviour for `start`.
|
|
60
62
|
const command = args.length === 0 && process.stdin.isTTY
|
|
61
|
-
? "
|
|
63
|
+
? "onboard"
|
|
62
64
|
: args[0] === "--help" || args[0] === "-h"
|
|
63
65
|
? "help"
|
|
64
66
|
: args[0] && !args[0].startsWith("-")
|
|
@@ -89,8 +91,8 @@ function printHelp() {
|
|
|
89
91
|
["runner start|status|stop", "Run PreMan's queued agent work on this machine"],
|
|
90
92
|
["doctor", "Diagnose credentials, backend, target, integrations"],
|
|
91
93
|
["install-desktop", "Download and install the PreMan desktop app"],
|
|
92
|
-
["onboard", "
|
|
93
|
-
["connect [options]", "Pick a coding agent and connect it"],
|
|
94
|
+
["onboard", "Create an account, install the app signed in, then integrations"],
|
|
95
|
+
["connect [options]", "Pick a coding agent and connect it (optional)"],
|
|
94
96
|
["dispatch [options]", "Let PreMan start agent runs for you"],
|
|
95
97
|
["aws | github | slack", "Connect one integration on its own"],
|
|
96
98
|
["login [--browser]", "Create/login to PreMan from the terminal"],
|
|
@@ -254,14 +256,13 @@ async function main() {
|
|
|
254
256
|
} else if (command === "dispatch") {
|
|
255
257
|
await dispatchCommand(commandArgs);
|
|
256
258
|
} else if (command === "onboard" || command === "setup") {
|
|
257
|
-
// makeArgs/authenticateTerminal/
|
|
259
|
+
// makeArgs/authenticateTerminal/the desktop pair are injected rather than
|
|
258
260
|
// imported there, so integrations.js stays free of a cycle back into the CLI.
|
|
259
261
|
await onboardCommand(commandArgs, {
|
|
260
262
|
makeArgs,
|
|
261
263
|
authenticateTerminal,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
runnerCommand,
|
|
264
|
+
installDesktop: installDesktopCommand,
|
|
265
|
+
openDesktopSignedIn,
|
|
265
266
|
});
|
|
266
267
|
} else if (command === "aws") {
|
|
267
268
|
await awsCommand(makeArgs(commandArgs));
|
package/bin/desktop.js
CHANGED
|
@@ -13,11 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
import { spawn, spawnSync } from "node:child_process";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
chmodSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
mkdtempSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from "node:fs";
|
|
17
26
|
import os from "node:os";
|
|
18
27
|
import path from "node:path";
|
|
19
28
|
|
|
20
|
-
import { cliInvocation, makeArgs } from "./shared.js";
|
|
29
|
+
import { CREDENTIALS_DIR, cliInvocation, makeArgs } from "./shared.js";
|
|
21
30
|
|
|
22
31
|
export const DESKTOP_HELP = `
|
|
23
32
|
Install-desktop options:
|
|
@@ -32,6 +41,80 @@ const RELEASE_API = "https://api.github.com/repos/PreMan-Inc/PreMan-Desktop/rele
|
|
|
32
41
|
export const APP_NAME = "PreMan.app";
|
|
33
42
|
const DOWNLOAD_TIMEOUT_MS = 300_000;
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Where the desktop app looks for a session the CLI just established.
|
|
46
|
+
*
|
|
47
|
+
* The key travels through a file rather than the `preman://` URL because a URL
|
|
48
|
+
* is handed to LaunchServices, which records it; the file sits in the directory
|
|
49
|
+
* that already holds the same key, with the same 0600 mode, so this adds no new
|
|
50
|
+
* class of exposure. The app deletes it on read, which is what makes it
|
|
51
|
+
* single-use -- the CLI cannot know whether an installed app is new enough to
|
|
52
|
+
* consume it, so expiry is enforced on the reading side.
|
|
53
|
+
*/
|
|
54
|
+
export const DESKTOP_SESSION_FILE = path.join(CREDENTIALS_DIR, "desktop-session.json");
|
|
55
|
+
|
|
56
|
+
export function installedAppPath(destination = "/Applications") {
|
|
57
|
+
return path.join(destination, APP_NAME);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function desktopAppInstalled(destination = "/Applications") {
|
|
61
|
+
return process.platform === "darwin" && existsSync(installedAppPath(destination));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hand the account the CLI just signed in to over to the desktop app.
|
|
66
|
+
*
|
|
67
|
+
* Returns what actually happened rather than a boolean, because the caller has
|
|
68
|
+
* something different to say in each case: an app that is not installed is not
|
|
69
|
+
* a failure, it is the fallback where the customer signs in themselves.
|
|
70
|
+
*/
|
|
71
|
+
export function writeDesktopSession(creds) {
|
|
72
|
+
const apiKey = String(creds?.api_key || "").trim();
|
|
73
|
+
if (!apiKey.startsWith("pm_live_")) return { state: "no-key" };
|
|
74
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
75
|
+
writeFileSync(
|
|
76
|
+
DESKTOP_SESSION_FILE,
|
|
77
|
+
`${JSON.stringify({ api_key: apiKey, user_email: creds?.user_email ?? null, created_at: new Date().toISOString() }, null, 2)}\n`,
|
|
78
|
+
{ mode: 0o600 }
|
|
79
|
+
);
|
|
80
|
+
return { state: "written", path: DESKTOP_SESSION_FILE };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Install-or-not aside, get the customer into the app signed in.
|
|
85
|
+
*
|
|
86
|
+
* The session is written before the app is launched so a cold start finds it on
|
|
87
|
+
* first read, rather than racing a window that is already loading.
|
|
88
|
+
*
|
|
89
|
+
* Whether it was taken up is then waited for rather than assumed: the app
|
|
90
|
+
* deletes the file as it reads it, and an app too old to know about the file at
|
|
91
|
+
* all would otherwise be reported as signed in while the customer looks at a
|
|
92
|
+
* login screen. The file is left behind on timeout -- it expires on its own, and
|
|
93
|
+
* a slow first launch can still find it.
|
|
94
|
+
*/
|
|
95
|
+
export async function openDesktopSignedIn(
|
|
96
|
+
creds,
|
|
97
|
+
{ destination = "/Applications", waitMs = 12_000, sleep = defaultSleep } = {}
|
|
98
|
+
) {
|
|
99
|
+
if (!desktopAppInstalled(destination)) {
|
|
100
|
+
return { state: "not-installed" };
|
|
101
|
+
}
|
|
102
|
+
const handoff = writeDesktopSession(creds);
|
|
103
|
+
spawn("open", ["-a", "PreMan"], { stdio: "ignore", detached: true }).unref();
|
|
104
|
+
if (handoff.state !== "written") return { state: "opened", handoff: handoff.state };
|
|
105
|
+
|
|
106
|
+
const deadline = Date.now() + waitMs;
|
|
107
|
+
while (Date.now() < deadline) {
|
|
108
|
+
await sleep(500);
|
|
109
|
+
if (!existsSync(DESKTOP_SESSION_FILE)) {
|
|
110
|
+
return { state: "opened-signed-in", handoff: handoff.state };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { state: "opened-not-adopted", handoff: handoff.state };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
|
|
35
118
|
/**
|
|
36
119
|
* Open a Playground session URL in PreMan.app on macOS when it is installed.
|
|
37
120
|
* Windows/Linux (and Mac without the app) get the website URL printed instead.
|
|
@@ -158,6 +241,34 @@ function mountedVolume(hdiutilOutput) {
|
|
|
158
241
|
return match[1].trim();
|
|
159
242
|
}
|
|
160
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Attach the disk image somewhere we named, rather than wherever it landed.
|
|
246
|
+
*
|
|
247
|
+
* Asking for the mountpoint is what makes this unambiguous. Reading it back out
|
|
248
|
+
* of `hdiutil info` is not a substitute: that lists every image attached on the
|
|
249
|
+
* machine, so an unrelated volume -- an iOS restore image, someone else's dmg --
|
|
250
|
+
* could be searched for PreMan.app instead of the download.
|
|
251
|
+
*/
|
|
252
|
+
function attachDiskImage(dmgPath, mountPoint) {
|
|
253
|
+
mkdirSync(mountPoint, { recursive: true });
|
|
254
|
+
try {
|
|
255
|
+
run("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet", "-readonly", "-mountpoint", mountPoint]);
|
|
256
|
+
return mountPoint;
|
|
257
|
+
} catch {
|
|
258
|
+
// Images that refuse an explicit mountpoint still report where they went, so
|
|
259
|
+
// fall back to this attach's own output -- never to the global image list.
|
|
260
|
+
return mountedVolume(run("hdiutil", ["attach", dmgPath, "-nobrowse", "-readonly"]));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The bundle to copy out, found by name and then by extension. */
|
|
265
|
+
function appInsideVolume(volume) {
|
|
266
|
+
const named = path.join(volume, APP_NAME);
|
|
267
|
+
if (existsSync(named)) return named;
|
|
268
|
+
const bundle = readdirSync(volume).find((entry) => entry.endsWith(".app"));
|
|
269
|
+
return bundle ? path.join(volume, bundle) : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
161
272
|
export async function installDesktopCommand(commandArgs = []) {
|
|
162
273
|
const args = makeArgs(commandArgs);
|
|
163
274
|
|
|
@@ -212,12 +323,11 @@ export async function installDesktopCommand(commandArgs = []) {
|
|
|
212
323
|
process.stdout.write(" checksum unavailable for this release; skipping verification\n");
|
|
213
324
|
}
|
|
214
325
|
|
|
215
|
-
|
|
216
|
-
mounted = mountedVolume(attach || run("hdiutil", ["info"]));
|
|
326
|
+
mounted = attachDiskImage(dmgPath, path.join(workDir, "mnt"));
|
|
217
327
|
|
|
218
|
-
const source =
|
|
219
|
-
if (!
|
|
220
|
-
throw new Error(
|
|
328
|
+
const source = appInsideVolume(mounted);
|
|
329
|
+
if (!source) {
|
|
330
|
+
throw new Error(`no application bundle found inside the disk image at ${mounted}`);
|
|
221
331
|
}
|
|
222
332
|
const target = path.join(destination, APP_NAME);
|
|
223
333
|
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
@@ -225,8 +335,8 @@ export async function installDesktopCommand(commandArgs = []) {
|
|
|
225
335
|
chmodSync(target, 0o755);
|
|
226
336
|
|
|
227
337
|
process.stdout.write(
|
|
228
|
-
`\nInstalled ${target}\n\nNext: open PreMan and
|
|
229
|
-
|
|
338
|
+
`\nInstalled ${target}\n\nNext: open PreMan and sign in with the account you use here.\n` +
|
|
339
|
+
`\`${cliInvocation()} onboard\` installs it and opens it already signed in.\n`
|
|
230
340
|
);
|
|
231
341
|
return { state: "installed", version, arch, path: target };
|
|
232
342
|
} finally {
|
package/bin/integrations.js
CHANGED
|
@@ -337,17 +337,6 @@ export async function slackCommand(args) {
|
|
|
337
337
|
// The guided run
|
|
338
338
|
// ---------------------------------------------------------------------------
|
|
339
339
|
|
|
340
|
-
/**
|
|
341
|
-
* Why a step that needs a connected agent cannot run on its own.
|
|
342
|
-
*
|
|
343
|
-
* Both of these drive the agent `connect` just linked, so skipping that step
|
|
344
|
-
* leaves them without one -- which is a thing to say plainly, with the command
|
|
345
|
-
* that does it later, rather than a stack trace about a missing id.
|
|
346
|
-
*/
|
|
347
|
-
function needsAgent(command) {
|
|
348
|
-
return `no coding agent connected yet -- run '${cliInvocation()} connect', then '${cliInvocation()} ${command}'`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
340
|
/** "yes" | "no" | "back" -- back only offered once there is somewhere to go. */
|
|
352
341
|
async function askStep(question, { assumeYes, canGoBack }) {
|
|
353
342
|
if (assumeYes) return "yes";
|
|
@@ -368,7 +357,7 @@ async function askStep(question, { assumeYes, canGoBack }) {
|
|
|
368
357
|
*/
|
|
369
358
|
export async function onboardCommand(
|
|
370
359
|
commandArgs,
|
|
371
|
-
{ makeArgs, authenticateTerminal,
|
|
360
|
+
{ makeArgs, authenticateTerminal, installDesktop, openDesktopSignedIn }
|
|
372
361
|
) {
|
|
373
362
|
const args = makeArgs(commandArgs);
|
|
374
363
|
const assumeYes = args.has("--yes");
|
|
@@ -378,47 +367,45 @@ export async function onboardCommand(
|
|
|
378
367
|
const creds = await authenticateTerminal(args);
|
|
379
368
|
connected(`Signed in as ${creds.user_email || "your account"}.`);
|
|
380
369
|
|
|
381
|
-
// Which agent the endpoints and runner steps drive. `connect` decided it, by
|
|
382
|
-
// detection or by asking, and this is the answer rather than a second prompt.
|
|
383
|
-
let linked = null;
|
|
384
|
-
|
|
385
370
|
const steps = [
|
|
386
371
|
{
|
|
387
|
-
name: "
|
|
388
|
-
question: "
|
|
372
|
+
name: "PreMan app",
|
|
373
|
+
question: "Install the PreMan app and open it signed in?",
|
|
389
374
|
run: async () => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
375
|
+
const installed = await installDesktop([...commandArgs]);
|
|
376
|
+
if (installed?.state === "unsupported") {
|
|
377
|
+
// Not a failure: the download link has already been printed, and the
|
|
378
|
+
// account this step exists to create is finished either way.
|
|
379
|
+
process.stdout.write("Sign in there with the account you just used.\n");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
process.stdout.write("Opening PreMan\u2026\n");
|
|
383
|
+
const opened = await openDesktopSignedIn(creds);
|
|
384
|
+
if (opened.state === "opened-signed-in") {
|
|
385
|
+
process.stdout.write("Opened PreMan, signed in as this account.\n");
|
|
386
|
+
} else if (opened.state === "not-installed") {
|
|
387
|
+
process.stdout.write(
|
|
388
|
+
"PreMan is not in /Applications yet \u2014 open it once installed and sign in.\n"
|
|
389
|
+
);
|
|
390
|
+
} else {
|
|
391
|
+
// Either the session could not be handed over or this app is too old to
|
|
392
|
+
// take it up. Say so rather than let the customer wonder why they are
|
|
393
|
+
// looking at a login screen.
|
|
394
|
+
process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
|
|
395
|
+
}
|
|
407
396
|
},
|
|
408
397
|
},
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{ name: "Slack", question: "Connect Slack?", run: () => slackCommand(args) },
|
|
398
|
+
// GitHub, AWS and Slack are deliberately not here. Starting out is an account
|
|
399
|
+
// and the app; each integration is its own command for whenever it is wanted.
|
|
412
400
|
];
|
|
413
401
|
|
|
414
402
|
// Outcome per step rather than three lists, so revisiting a step replaces its
|
|
415
403
|
// result instead of recording it twice.
|
|
416
404
|
const outcome = new Map();
|
|
417
405
|
|
|
418
|
-
// Indexed rather than for..of so "back" can move the cursor. Re-running a
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// is detected rather than duplicated.
|
|
406
|
+
// Indexed rather than for..of so "back" can move the cursor. Re-running a step
|
|
407
|
+
// is safe: an install that already happened overwrites the same bundle rather
|
|
408
|
+
// than duplicating it.
|
|
422
409
|
let i = 0;
|
|
423
410
|
while (i < steps.length) {
|
|
424
411
|
const step = steps[i];
|
|
@@ -464,8 +451,8 @@ export async function onboardCommand(
|
|
|
464
451
|
|
|
465
452
|
export const INTEGRATIONS_HELP = `
|
|
466
453
|
Setup options:
|
|
467
|
-
preman onboard
|
|
468
|
-
|
|
454
|
+
preman onboard Create or sign in to an account, then install the
|
|
455
|
+
PreMan app and open it signed in
|
|
469
456
|
preman aws Connect an AWS account and stream a log group
|
|
470
457
|
preman github Install the PreMan GitHub App
|
|
471
458
|
preman slack Add PreMan to a Slack workspace
|
package/dist/server.js
CHANGED
|
@@ -1332,6 +1332,90 @@ export function createServer() {
|
|
|
1332
1332
|
});
|
|
1333
1333
|
}
|
|
1334
1334
|
});
|
|
1335
|
+
// ── Collections test generation ────────────────────────────────────
|
|
1336
|
+
// The dashboard's Generate Tests / Review flagged / Setup fixtures /
|
|
1337
|
+
// Enrich with agent buttons, so an agent can run them without clicking.
|
|
1338
|
+
server.tool("generate_saved_tests", "Generate scheduled test suites for every endpoint already saved in a PreMan workspace. This is the dashboard's Collections 'Generate Tests' button: it clones the workspace's connected GitHub repository, matches the discovered routes onto the saved endpoints by method and path, and writes heuristic cases onto the ones it matches. No model is involved, so nothing invents a body for a DELETE. Write, destructive, low-confidence, and path-parameter routes come back flagged and disabled — clear them with review_generated_tests. This is not generate_tests, which writes cases for a single endpoint you describe.", {
|
|
1339
|
+
action: z
|
|
1340
|
+
.enum(["generate", "status"])
|
|
1341
|
+
.optional()
|
|
1342
|
+
.describe("generate (default) runs the scan; status is read-only counts"),
|
|
1343
|
+
integration_id: z
|
|
1344
|
+
.string()
|
|
1345
|
+
.optional()
|
|
1346
|
+
.describe("Connected repo to scan. Omit to use the one with the most synced endpoints."),
|
|
1347
|
+
workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
|
|
1348
|
+
}, async (args) => {
|
|
1349
|
+
try {
|
|
1350
|
+
const result = await callBackend("generate_saved_tests", args);
|
|
1351
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1352
|
+
}
|
|
1353
|
+
catch (e) {
|
|
1354
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1355
|
+
next_actions: [
|
|
1356
|
+
"Connect a GitHub repository to this workspace in Settings → Integrations, then retry.",
|
|
1357
|
+
],
|
|
1358
|
+
related_tools: ["review_generated_tests", "setup_test_fixtures", "enrich_generated_tests"],
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
server.tool("review_generated_tests", "List and approve the generated test suites PreMan flagged for human review. This is the dashboard's 'Review flagged' button. generate_saved_tests deliberately leaves mutating and ambiguous routes disabled; this is how a reviewed suite gets turned on. Approving enables the suite's read-only schedule and does not raise the unattended write policy, so a DELETE stays gated at run time. List first and show the user each suite and its reason; only use approve_all when they asked to clear the whole queue.", {
|
|
1363
|
+
action: z
|
|
1364
|
+
.enum(["list", "approve", "approve_all"])
|
|
1365
|
+
.optional()
|
|
1366
|
+
.describe("list (default) | approve one request_id | approve_all (max 50)"),
|
|
1367
|
+
request_id: z.string().optional().describe("Saved request to approve; required for approve"),
|
|
1368
|
+
workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
|
|
1369
|
+
}, async (args) => {
|
|
1370
|
+
try {
|
|
1371
|
+
const result = await callBackend("review_generated_tests", args);
|
|
1372
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1373
|
+
}
|
|
1374
|
+
catch (e) {
|
|
1375
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1376
|
+
next_actions: ["Call review_generated_tests with action='list' to see the queue."],
|
|
1377
|
+
related_tools: ["generate_saved_tests", "enrich_generated_tests"],
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
server.tool("setup_test_fixtures", "Fill the {id} path parameters in saved PreMan endpoints with real record ids. This is the dashboard's 'Setup fixtures' button: a happy-path test for GET /users/{id} needs an id that exists, so PreMan calls the sibling list route read-only, harvests ids from the response, and falls back to the endpoint's schema example. It never mints a fake UUID that would only 404 and never fires a write. Harvested values are stored in workspace settings and returned as a .env snippet. This does not approve anything.", {
|
|
1382
|
+
action: z
|
|
1383
|
+
.enum(["setup", "list"])
|
|
1384
|
+
.optional()
|
|
1385
|
+
.describe("setup (default) harvests ids; list reports what is still missing"),
|
|
1386
|
+
workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
|
|
1387
|
+
}, async (args) => {
|
|
1388
|
+
try {
|
|
1389
|
+
const result = await callBackend("setup_test_fixtures", args);
|
|
1390
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1391
|
+
}
|
|
1392
|
+
catch (e) {
|
|
1393
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1394
|
+
next_actions: ["Save at least one endpoint with a {id} path parameter, then retry."],
|
|
1395
|
+
related_tools: ["generate_saved_tests", "review_generated_tests"],
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
server.tool("enrich_generated_tests", "Add model-written test cases on top of PreMan's heuristic suites. This is the dashboard's 'Enrich with agent' button. generate_saved_tests writes deterministic cases only; this pass asks a model for a few extra edge cases per suite. It touches review-cleared suites only, skips destructive and billing-sensitive requests unless workspace policy already allows that risk, skips suites already enriched, and stops after 40 suites per call so a large collection cannot fan out into hundreds of model calls. Leftovers come back as remaining.", {
|
|
1400
|
+
action: z
|
|
1401
|
+
.enum(["enrich", "list"])
|
|
1402
|
+
.optional()
|
|
1403
|
+
.describe("enrich (default) runs the pass; list returns the eligible suites"),
|
|
1404
|
+
workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
|
|
1405
|
+
}, async (args) => {
|
|
1406
|
+
try {
|
|
1407
|
+
const result = await callBackend("enrich_generated_tests", args);
|
|
1408
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1409
|
+
}
|
|
1410
|
+
catch (e) {
|
|
1411
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
1412
|
+
next_actions: [
|
|
1413
|
+
"Run generate_saved_tests first, then clear the review queue with review_generated_tests.",
|
|
1414
|
+
],
|
|
1415
|
+
related_tools: ["generate_saved_tests", "review_generated_tests"],
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1335
1419
|
server.tool("share_endpoints_with_ui", "Push discovered or verified endpoints into the PreMan Playground so the user can see them, test them, and convert selected endpoints into hosted MCP tools. Use this after verify_endpoints_live or when the user explicitly asks to stream endpoints to the UI.", {
|
|
1336
1420
|
endpoints: z.array(z.any()).describe("Endpoints to push. Each item should include method plus path/path_template/url; include schemas when available."),
|
|
1337
1421
|
upstream_base_url: z.string().optional().describe("Default upstream base URL for testing and MCP generation, e.g. http://127.0.0.1:8000 or https://api.example.com"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"dev": "tsx src/server.ts",
|
|
14
14
|
"open-mcp-preview": "node scripts/emit-mcp-preview.mjs",
|
|
15
15
|
"open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
|
|
16
|
-
"test": "npm run build && npm run test:connect && npm run test:node",
|
|
16
|
+
"test": "npm run build && npm run test:connect && npm run test:node && npm run test:dmg",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
|
-
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs"
|
|
18
|
+
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs",
|
|
19
|
+
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@modelcontextprotocol/ext-apps": "^0.1.0",
|