premanmcp 0.10.7 → 0.11.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/bin/api_tools.js +8 -0
- package/bin/connect/guide.js +13 -1
- package/bin/desktop.js +27 -2
- package/dist/server.js +59 -5
- package/package.json +1 -1
package/bin/api_tools.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
|
|
11
11
|
import { callBackendJson, cliInvocation, makeArgs, resolveApiKey } from "./shared.js";
|
|
12
|
+
import { printPlayground } from "./desktop.js";
|
|
12
13
|
|
|
13
14
|
export const ENDPOINTS_HELP = `
|
|
14
15
|
Endpoints:
|
|
@@ -144,6 +145,13 @@ export async function endpointsCommand(commandArgs) {
|
|
|
144
145
|
if (requests.length) {
|
|
145
146
|
process.stdout.write(`runnable as: ${requests.join(", ")}\n`);
|
|
146
147
|
}
|
|
148
|
+
const url = result.ui?.url || result.url;
|
|
149
|
+
printPlayground(url);
|
|
150
|
+
if (result.campaign?.id) {
|
|
151
|
+
process.stdout.write(
|
|
152
|
+
`test campaign ${result.campaign.id}: ${result.campaign.queued || 0} queued, ${result.campaign.skipped || 0} skipped\n`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
147
155
|
return undefined;
|
|
148
156
|
}
|
|
149
157
|
|
package/bin/connect/guide.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from "node:path";
|
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { callTool as callPremanTool, printTestSummary } from "../api_tools.js";
|
|
15
|
-
import { installDesktopCommand } from "../desktop.js";
|
|
15
|
+
import { installDesktopCommand, printPlayground } from "../desktop.js";
|
|
16
16
|
import { hookStatus, installHook } from "../hook.js";
|
|
17
17
|
import { MARK, awsCommand, githubCommand, slackCommand } from "../integrations.js";
|
|
18
18
|
import { confirmRunnerOnline, pairingIsLive, readRunnerState, registerRunner, runnerIsAlive, startBackground } from "../runner.js";
|
|
@@ -198,6 +198,18 @@ export async function discoverEndpoints(
|
|
|
198
198
|
process.stdout.write(
|
|
199
199
|
`${MARK.ok()} ${after.registered} endpoint(s) · ${after.runnable} runnable\n`
|
|
200
200
|
);
|
|
201
|
+
try {
|
|
202
|
+
const listed = await callPremanTool(args, "get_endpoints", {
|
|
203
|
+
limit: 100,
|
|
204
|
+
include_schemas: true,
|
|
205
|
+
});
|
|
206
|
+
const share = await callPremanTool(args, "share_endpoints_with_ui", {
|
|
207
|
+
endpoints: listed.endpoints || [],
|
|
208
|
+
});
|
|
209
|
+
printPlayground(share.ui?.url || share.url);
|
|
210
|
+
} catch {
|
|
211
|
+
// Mapping succeeded; a missing Playground link must not look like discovery failed.
|
|
212
|
+
}
|
|
201
213
|
return after;
|
|
202
214
|
} catch (error) {
|
|
203
215
|
process.stdout.write(`${MARK.fail()} Could not read your endpoints: ${error.message}\n`);
|
package/bin/desktop.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* has to do.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
17
|
import os from "node:os";
|
|
@@ -29,9 +29,34 @@ Install-desktop options:
|
|
|
29
29
|
|
|
30
30
|
const RELEASES_BASE = "https://github.com/PreMan-Inc/PreMan-Desktop/releases";
|
|
31
31
|
const RELEASE_API = "https://api.github.com/repos/PreMan-Inc/PreMan-Desktop/releases/latest";
|
|
32
|
-
const APP_NAME = "PreMan.app";
|
|
32
|
+
export const APP_NAME = "PreMan.app";
|
|
33
33
|
const DOWNLOAD_TIMEOUT_MS = 300_000;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Open a Playground session URL in PreMan.app on macOS when it is installed.
|
|
37
|
+
* Windows/Linux (and Mac without the app) get the website URL printed instead.
|
|
38
|
+
*/
|
|
39
|
+
export function openPlayground(url) {
|
|
40
|
+
const target = String(url || "").trim();
|
|
41
|
+
if (!target) return "none";
|
|
42
|
+
const app = path.join("/Applications", APP_NAME);
|
|
43
|
+
if (process.platform === "darwin" && existsSync(app)) {
|
|
44
|
+
spawn("open", ["-a", "PreMan", target], { stdio: "ignore", detached: true }).unref();
|
|
45
|
+
return "desktop";
|
|
46
|
+
}
|
|
47
|
+
return "web";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function printPlayground(url) {
|
|
51
|
+
const target = String(url || "").trim();
|
|
52
|
+
if (!target) return;
|
|
53
|
+
const where = openPlayground(target);
|
|
54
|
+
process.stdout.write(`Watch it: ${target}\n`);
|
|
55
|
+
if (where === "desktop") {
|
|
56
|
+
process.stdout.write("Opened in PreMan desktop\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
export function dmgUrl(arch) {
|
|
36
61
|
return `${RELEASES_BASE}/latest/download/PreMan-mac-${arch}.dmg`;
|
|
37
62
|
}
|
package/dist/server.js
CHANGED
|
@@ -381,7 +381,8 @@ export function createServer() {
|
|
|
381
381
|
"",
|
|
382
382
|
"## Tool taxonomy",
|
|
383
383
|
"- **Read tools** (no side effects): get_endpoints, get_coverage, detect_drift, preman_status, list_collections, get_collection, list_runs",
|
|
384
|
-
"- **Action tools** (execute tests / mutate state): test_api, generate_tests, generate_endpoint_tests, run_stress_test, import_collection, test_endpoint_by_id, register_discovered_endpoints, run_tests, delete_collection",
|
|
384
|
+
"- **Action tools** (execute tests / mutate state): test_api, generate_tests, generate_endpoint_tests, run_stress_test, start_test_campaign, import_collection, test_endpoint_by_id, register_discovered_endpoints, run_tests, delete_collection",
|
|
385
|
+
"- **Create-then-delete contracts**: propose_lifecycle_contracts (propose a POST/DELETE pairing), approve_lifecycle_contracts (user decision — ask first)",
|
|
385
386
|
"- **MCP conversion tools**: discover_endpoints_from_codebase, verify_endpoints_live, mcp_preview (returns inline two-pane panel), mcp_deploy, mcp_list_deployed, mcp_mint_consumer_token, mcp_revoke_consumer_token",
|
|
386
387
|
"- **Auth (API key / PreMan)**: preman_create_api_key (JWT -> saved pm_live_ key), preman_login, preman_login_complete, preman_logout",
|
|
387
388
|
"- **Auth (app JWT — email/OTP/password on your API)**: user_auth_start_signup, user_auth_signup, user_auth_verify_otp, user_auth_login, user_auth_needs_password, user_auth_resend_otp, user_auth_forgot_password, user_auth_set_password, user_auth_me, user_auth_change_password, user_auth_delete_account (HTTP to PREMAN_BACKEND /auth/*; no pm_live_ key required)",
|
|
@@ -396,7 +397,8 @@ export function createServer() {
|
|
|
396
397
|
"- 'migrate / move / switch from Postman', 'bring my Postman workspace over': migrate_from_postman — it imports AND creates scheduled suites, environments and alerts in one call. Prefer it over import_collection whenever the user frames this as leaving Postman.",
|
|
397
398
|
"- 'generate tests for endpoint X' / 'write unit tests for X and run them': generate_endpoint_tests (set include_code=true and write the returned code_artifacts to disk when the user wants test files).",
|
|
398
399
|
"- 'stress / load test X': run_stress_test. Read-only unless the user explicitly authorises writes — never set allow_writes yourself.",
|
|
399
|
-
"- After discover_endpoints_from_codebase, call register_discovered_endpoints with the JSON array it asked for
|
|
400
|
+
"- After discover_endpoints_from_codebase, call register_discovered_endpoints with the JSON array it asked for. That streams endpoints to the Playground and starts a read-safe test campaign. Then verify_endpoints_live if you need a live probe.",
|
|
401
|
+
"- 'why aren't my POSTs tested?' / 'test the creates too': POSTs are skipped because nothing undoes them. Read the code, pair each POST with the DELETE that reverses it via propose_lifecycle_contracts, then ask the user to approve. Approve once and every campaign creates and deletes a real record. Never call approve_lifecycle_contracts without the user saying yes.",
|
|
400
402
|
"- App-auth flows (signup, OTP, login, change_password) live under the user_auth_* tools and do not need an pm_live_ key.",
|
|
401
403
|
"- PREMAN_BACKEND / backend_url is the PreMan control plane. It is NOT the target API upstream for a generated MCP unless the user's own API is PreMan. Prefer base_url from verify_endpoints_live results.",
|
|
402
404
|
"",
|
|
@@ -541,7 +543,7 @@ export function createServer() {
|
|
|
541
543
|
}
|
|
542
544
|
});
|
|
543
545
|
// ── register_discovered_endpoints ─────────────────────────────────
|
|
544
|
-
server.tool("register_discovered_endpoints", "Save discovered endpoints into PreMan
|
|
546
|
+
server.tool("register_discovered_endpoints", "Save discovered endpoints into PreMan, stream them to the Playground, and start a read-safe test campaign. Use after discover_endpoints_from_codebase. Mutating endpoints stay skipped until start_test_campaign(allow_writes=true).", {
|
|
545
547
|
endpoints: z.array(z.record(z.any())).optional().describe("Discovery-shaped endpoint objects (method, path_template, schemas, confidence, …); max 100"),
|
|
546
548
|
endpoint_ids: z.array(z.string()).optional().describe("Alternatively, existing registry ids to set up as runnable requests"),
|
|
547
549
|
project_id: z.string().optional().describe("Scope registration to a project"),
|
|
@@ -550,12 +552,64 @@ export function createServer() {
|
|
|
550
552
|
}, async (args) => {
|
|
551
553
|
try {
|
|
552
554
|
const result = await callBackend("register_discovered_endpoints", args);
|
|
553
|
-
return withFrontendUrl(result, "/
|
|
555
|
+
return withFrontendUrl(result, "/try");
|
|
554
556
|
}
|
|
555
557
|
catch (e) {
|
|
556
558
|
return toolError(e.message, inferErrorCode(e.message), {
|
|
557
559
|
next_actions: ["Run discover_endpoints_from_codebase first and pass its endpoints array here."],
|
|
558
|
-
related_tools: ["discover_endpoints_from_codebase", "verify_endpoints_live"],
|
|
560
|
+
related_tools: ["discover_endpoints_from_codebase", "start_test_campaign", "verify_endpoints_live"],
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
server.tool("start_test_campaign", "Fan out tests across registered endpoints: functional scenarios, GET-only stress, and security-lite negatives (missing auth / empty body). Read-only by default. Not a scanner or exploit runner.", {
|
|
565
|
+
endpoint_ids: z.array(z.string()).optional().describe("Registry ids from register_discovered_endpoints"),
|
|
566
|
+
allow_writes: z.boolean().optional().default(false).describe("Permit POST/PUT/PATCH functional jobs; DELETE stays skipped"),
|
|
567
|
+
}, async (args) => {
|
|
568
|
+
try {
|
|
569
|
+
const result = await callBackend("start_test_campaign", args);
|
|
570
|
+
return withFrontendUrl(result, "/try");
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
574
|
+
next_actions: ["Call register_discovered_endpoints first, then pass its endpoint_ids."],
|
|
575
|
+
related_tools: ["register_discovered_endpoints", "get_endpoints"],
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// ── create-then-delete contracts ──────────────────────────────────
|
|
580
|
+
server.tool("propose_lifecycle_contracts", "Propose that a POST be tested for real by deleting what it creates. You read the customer's routes and response models, so you know which DELETE undoes which POST and where the new id appears. PreMan re-checks eligibility itself: billing-, auth- and notification-shaped creates are always rejected because a DELETE cannot claw back money or unsend an email. Proposals do nothing until the user approves them.", {
|
|
581
|
+
contracts: z
|
|
582
|
+
.array(z.object({
|
|
583
|
+
create_endpoint_id: z.string().describe("Registry id of the POST"),
|
|
584
|
+
cleanup_endpoint_id: z.string().describe("Registry id of the DELETE that undoes it"),
|
|
585
|
+
id_source: z.string().optional().describe("Where the new id sits in the create response, e.g. 'json.id' or 'json.data.id'"),
|
|
586
|
+
evidence: z.string().optional().describe("Why this pairing is correct (route/model you read)"),
|
|
587
|
+
}))
|
|
588
|
+
.describe("Pairings to propose; max 100"),
|
|
589
|
+
}, async (args) => {
|
|
590
|
+
try {
|
|
591
|
+
const result = await callBackend("propose_lifecycle_contracts", args);
|
|
592
|
+
return withFrontendUrl(result, "/try");
|
|
593
|
+
}
|
|
594
|
+
catch (e) {
|
|
595
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
596
|
+
next_actions: ["Register the endpoints first so both ids exist, then propose the pairing."],
|
|
597
|
+
related_tools: ["register_discovered_endpoints", "approve_lifecycle_contracts"],
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
server.tool("approve_lifecycle_contracts", "Approve (or reject) create-then-delete contracts. ASK THE USER FIRST — never approve on their behalf. An approved create runs for real on every campaign from then on, teardown included; the teardown is scoped to the id that run just created and can never delete anything else.", {
|
|
602
|
+
contract_ids: z.array(z.string()).describe("Ids from propose_lifecycle_contracts or register_discovered_endpoints' lifecycle_proposals"),
|
|
603
|
+
approve: z.boolean().optional().default(true).describe("False records a rejection so the pairing stops being re-proposed"),
|
|
604
|
+
}, async (args) => {
|
|
605
|
+
try {
|
|
606
|
+
const result = await callBackend("approve_lifecycle_contracts", args);
|
|
607
|
+
return withFrontendUrl(result, "/try");
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
611
|
+
next_actions: ["Confirm with the user which POST endpoints they want exercised for real, then retry."],
|
|
612
|
+
related_tools: ["propose_lifecycle_contracts", "start_test_campaign"],
|
|
559
613
|
});
|
|
560
614
|
}
|
|
561
615
|
});
|