premanmcp 0.13.0 → 0.15.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 +42 -6
- package/bin/account.js +5 -0
- package/bin/cli.js +3 -2
- package/bin/connect/guide.js +85 -23
- package/bin/connect.js +8 -0
- package/bin/desktop.js +47 -14
- package/bin/detect.js +59 -3
- package/bin/hook.js +12 -6
- package/bin/integrations.js +283 -46
- package/bin/link.js +207 -0
- package/bin/repo.js +193 -0
- package/bin/shared.js +76 -13
- package/bin/status.js +35 -0
- package/bin/verify.js +125 -8
- package/dist/server.js +192 -3
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -384,6 +384,7 @@ export function createServer() {
|
|
|
384
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
385
|
"- **Create-then-delete contracts**: propose_lifecycle_contracts (propose a POST/DELETE pairing), approve_lifecycle_contracts (user decision — ask first)",
|
|
386
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",
|
|
387
|
+
"- **SDK tools**: generate_sdk (builds a client from endpoints PreMan already discovered - takes no OpenAPI file), get_sdk_generation (poll a run), get_sdk_languages (roster + whether the toolchain is present)",
|
|
387
388
|
"- **Auth (API key / PreMan)**: preman_create_api_key (JWT -> saved pm_live_ key), preman_login, preman_login_complete, preman_logout",
|
|
388
389
|
"- **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)",
|
|
389
390
|
"- **Auth to PreMan Playground**: share_user_auth_flow_with_ui pushes signup, verify-otp, login, resend-otp with JSON schemas to the signed-in Playground. Requires preman_login / PREMAN_API_KEY so the session appears in the user's dashboard.",
|
|
@@ -392,6 +393,7 @@ export function createServer() {
|
|
|
392
393
|
"- 'scan / discover / find / list my endpoints' (codebase): use discover_endpoints_from_codebase. NEVER use get_endpoints for this — that returns inventory already saved in PreMan, not source code.",
|
|
393
394
|
"- 'list endpoints already saved in my PreMan dashboard': use get_endpoints. NEVER use discover_endpoints_from_codebase for this.",
|
|
394
395
|
"- 'preview my MCP before deploying': mcp_preview (read-only). 'actually deploy': mcp_deploy.",
|
|
396
|
+
"- 'generate an SDK / client library for my API', 'I use Speakeasy / Fern': generate_sdk. Do NOT ask the user for an OpenAPI file or run a spec export first - PreMan builds the spec from the endpoints it already discovered, and needing a file is the chore this replaces. Pass open_pr=true only when the user asks for a pull request rather than a download.",
|
|
395
397
|
"- 'test the login endpoint' / 'hit POST /auth/login with body': test_api with the full URL. Use test_endpoint_by_id ONLY when the user references a saved endpoint by id/uuid.",
|
|
396
398
|
"- 'import my Postman/OpenAPI/Bruno/curl collection': import_collection.",
|
|
397
399
|
"- '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.",
|
|
@@ -699,6 +701,193 @@ export function createServer() {
|
|
|
699
701
|
});
|
|
700
702
|
}
|
|
701
703
|
});
|
|
704
|
+
// ── generate_sdk ──────────────────────────────────────────────────
|
|
705
|
+
//
|
|
706
|
+
// Straight to the REST API rather than through /mcp/call-tool, because the
|
|
707
|
+
// SDK endpoints take the same pm_live_ key and adding a backend tool wrapper
|
|
708
|
+
// would buy nothing. The spec is deliberately not a parameter: PreMan already
|
|
709
|
+
// holds one for every connected repository, and asking an agent to supply the
|
|
710
|
+
// document describing the API PreMan has been scanning is the exact chore
|
|
711
|
+
// this feature exists to remove.
|
|
712
|
+
async function sdkFetch(method, path, body) {
|
|
713
|
+
requireAuth();
|
|
714
|
+
const resp = await fetch(`${BACKEND_URL}${path}`, {
|
|
715
|
+
method,
|
|
716
|
+
headers: {
|
|
717
|
+
"Content-Type": "application/json",
|
|
718
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
719
|
+
},
|
|
720
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
721
|
+
});
|
|
722
|
+
const text = await resp.text();
|
|
723
|
+
let parsed = null;
|
|
724
|
+
try {
|
|
725
|
+
parsed = text ? JSON.parse(text) : null;
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
parsed = { raw: text };
|
|
729
|
+
}
|
|
730
|
+
if (!resp.ok) {
|
|
731
|
+
const detail = (parsed && (parsed.detail?.[0]?.msg || parsed.detail)) || text || resp.statusText;
|
|
732
|
+
throw new Error(`${resp.status} ${typeof detail === "string" ? detail : JSON.stringify(detail)}`);
|
|
733
|
+
}
|
|
734
|
+
return parsed;
|
|
735
|
+
}
|
|
736
|
+
/** Resolve a repo name like "acme/api" to a connected integration id. */
|
|
737
|
+
async function resolveIntegration(hint) {
|
|
738
|
+
const rows = await sdkFetch("GET", "/integrations/github");
|
|
739
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
740
|
+
throw new Error("no GitHub repository is connected; connect one before generating an SDK");
|
|
741
|
+
}
|
|
742
|
+
if (!hint) {
|
|
743
|
+
if (rows.length === 1)
|
|
744
|
+
return rows[0];
|
|
745
|
+
const names = rows.map((r) => r.repo_url).join(", ");
|
|
746
|
+
throw new Error(`several repositories are connected, so name one with repo: ${names}`);
|
|
747
|
+
}
|
|
748
|
+
const needle = hint.toLowerCase();
|
|
749
|
+
const hit = rows.find((r) => String(r.id) === hint) ||
|
|
750
|
+
rows.find((r) => String(r.repo_url || "").toLowerCase().includes(needle));
|
|
751
|
+
if (!hit) {
|
|
752
|
+
throw new Error(`no connected repository matches "${hint}"; connected: ${rows
|
|
753
|
+
.map((r) => r.repo_url)
|
|
754
|
+
.join(", ")}`);
|
|
755
|
+
}
|
|
756
|
+
return hit;
|
|
757
|
+
}
|
|
758
|
+
server.tool("generate_sdk", "Generate a client SDK from the endpoints PreMan already discovered for a connected repository. Takes no OpenAPI file - PreMan builds the spec itself. Returns a download URL. Use open_pr=true to open a pull request instead of producing an archive.", {
|
|
759
|
+
repo: z
|
|
760
|
+
.string()
|
|
761
|
+
.optional()
|
|
762
|
+
.describe("Which connected repository, as an integration id or any part of the repo URL such as 'acme/api'. Optional when only one is connected."),
|
|
763
|
+
languages: z
|
|
764
|
+
.array(z.string())
|
|
765
|
+
.optional()
|
|
766
|
+
.describe("Languages to generate. Defaults to ['python']. Call get_sdk_languages for the roster."),
|
|
767
|
+
halves: z
|
|
768
|
+
.array(z.enum(["client", "server"]))
|
|
769
|
+
.optional()
|
|
770
|
+
.describe("Client, server, or both. Defaults to ['client'], which is what a consumer of the API needs."),
|
|
771
|
+
open_pr: z
|
|
772
|
+
.boolean()
|
|
773
|
+
.optional()
|
|
774
|
+
.default(false)
|
|
775
|
+
.describe("Open a pull request against the repository instead of returning an archive. Requires the repository to have opted in to automatic pull requests."),
|
|
776
|
+
wait_seconds: z
|
|
777
|
+
.number()
|
|
778
|
+
.optional()
|
|
779
|
+
.default(150)
|
|
780
|
+
.describe("How long to wait for the run before handing back an id to poll. 0 returns immediately."),
|
|
781
|
+
}, async (args) => {
|
|
782
|
+
try {
|
|
783
|
+
const integration = await resolveIntegration(args.repo);
|
|
784
|
+
const integrationId = String(integration.id);
|
|
785
|
+
// Read the spec first so an empty or fully excluded surface is reported
|
|
786
|
+
// as that, rather than as a generation which fails a minute later.
|
|
787
|
+
const preview = await sdkFetch("GET", `/sdk/integrations/${integrationId}/spec`);
|
|
788
|
+
const run = await sdkFetch("POST", "/sdk/generate", {
|
|
789
|
+
integration_id: integrationId,
|
|
790
|
+
languages: args.languages ?? ["python"],
|
|
791
|
+
halves: args.halves ?? ["client"],
|
|
792
|
+
open_pr: args.open_pr ?? false,
|
|
793
|
+
});
|
|
794
|
+
const dashPath = "/endpoints";
|
|
795
|
+
emitActivity("navigate", { path: dashPath });
|
|
796
|
+
const deadline = Date.now() + Math.max(0, args.wait_seconds ?? 150) * 1000;
|
|
797
|
+
let current = run;
|
|
798
|
+
while (Date.now() < deadline &&
|
|
799
|
+
!["succeeded", "failed", "cancelled"].includes(String(current.status))) {
|
|
800
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
801
|
+
current = await sdkFetch("GET", `/sdk/generations/${run.id}`);
|
|
802
|
+
}
|
|
803
|
+
const done = String(current.status) === "succeeded";
|
|
804
|
+
return withFrontendUrl({
|
|
805
|
+
...agentSessionPayload(dashPath),
|
|
806
|
+
generation_id: run.id,
|
|
807
|
+
status: current.status,
|
|
808
|
+
repo: integration.repo_url,
|
|
809
|
+
operations: preview.operation_count,
|
|
810
|
+
endpoints_published: preview.endpoint_count,
|
|
811
|
+
endpoints_excluded: preview.excluded_count,
|
|
812
|
+
spec_warnings: preview.warnings ?? [],
|
|
813
|
+
languages: current.languages,
|
|
814
|
+
files: current.files ?? [],
|
|
815
|
+
pr_url: current.pr_url ?? null,
|
|
816
|
+
error: current.error ?? null,
|
|
817
|
+
download_url: done
|
|
818
|
+
? `${BACKEND_URL}/sdk/generations/${run.id}/download`
|
|
819
|
+
: null,
|
|
820
|
+
_agent_hints: done
|
|
821
|
+
? {
|
|
822
|
+
next_actions: [
|
|
823
|
+
`Download the archive: curl -H "Authorization: Bearer $PREMAN_API_KEY" -o sdk.zip ${BACKEND_URL}/sdk/generations/${run.id}/download`,
|
|
824
|
+
],
|
|
825
|
+
}
|
|
826
|
+
: {
|
|
827
|
+
next_actions: [
|
|
828
|
+
`Still ${current.status}. Poll with get_sdk_generation(generation_id="${run.id}").`,
|
|
829
|
+
],
|
|
830
|
+
},
|
|
831
|
+
}, dashPath);
|
|
832
|
+
}
|
|
833
|
+
catch (e) {
|
|
834
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
835
|
+
next_actions: [
|
|
836
|
+
"Confirm a repository is connected and has discovered endpoints (get_endpoints).",
|
|
837
|
+
"If open_pr was true, the repository must have opted in to automatic pull requests.",
|
|
838
|
+
],
|
|
839
|
+
related_tools: ["get_endpoints", "get_sdk_languages", "get_sdk_generation"],
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
// ── get_sdk_generation ────────────────────────────────────────────
|
|
844
|
+
server.tool("get_sdk_generation", "Poll one SDK generation run started by generate_sdk. Returns status and, once finished, the download URL.", {
|
|
845
|
+
generation_id: z.string().describe("The generation_id returned by generate_sdk"),
|
|
846
|
+
}, async (args) => {
|
|
847
|
+
try {
|
|
848
|
+
const run = await sdkFetch("GET", `/sdk/generations/${args.generation_id}`);
|
|
849
|
+
const done = String(run.status) === "succeeded";
|
|
850
|
+
return {
|
|
851
|
+
content: [
|
|
852
|
+
{
|
|
853
|
+
type: "text",
|
|
854
|
+
text: JSON.stringify({
|
|
855
|
+
generation_id: run.id,
|
|
856
|
+
status: run.status,
|
|
857
|
+
attempts: run.attempts,
|
|
858
|
+
error: run.error ?? null,
|
|
859
|
+
pr_url: run.pr_url ?? null,
|
|
860
|
+
files: run.files ?? [],
|
|
861
|
+
download_url: done
|
|
862
|
+
? `${BACKEND_URL}/sdk/generations/${run.id}/download`
|
|
863
|
+
: null,
|
|
864
|
+
}, null, 2),
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
catch (e) {
|
|
870
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
871
|
+
related_tools: ["generate_sdk"],
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
// ── get_sdk_languages ─────────────────────────────────────────────
|
|
876
|
+
server.tool("get_sdk_languages", "Which languages this PreMan deployment can generate, and whether the generator toolchain is present at all.", {}, async () => {
|
|
877
|
+
try {
|
|
878
|
+
const roster = await sdkFetch("GET", "/sdk/languages");
|
|
879
|
+
return {
|
|
880
|
+
content: [
|
|
881
|
+
{ type: "text", text: JSON.stringify(roster, null, 2) },
|
|
882
|
+
],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (e) {
|
|
886
|
+
return toolError(e.message, inferErrorCode(e.message), {
|
|
887
|
+
related_tools: ["generate_sdk"],
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
702
891
|
// ── import_collection ─────────────────────────────────────────────
|
|
703
892
|
server.tool("import_collection", "Import a Postman, OpenAPI, Bruno, or curl collection. Converts to .ot.yaml and saves. Returns slim summary (name, endpoint list, collection_id). Set include_yaml=true to get the full YAML string.", {
|
|
704
893
|
content: z.string().describe("Raw collection content (Postman JSON, OpenAPI YAML, Bruno .bru, curl command)"),
|
|
@@ -1517,14 +1706,14 @@ export function createServer() {
|
|
|
1517
1706
|
});
|
|
1518
1707
|
}
|
|
1519
1708
|
});
|
|
1520
|
-
server.tool("mcp_deploy", "Create and host an MCP server from a preview spec plus an upstream credential. By default
|
|
1709
|
+
server.tool("mcp_deploy", "Create and host an MCP server from a preview spec plus an upstream credential. Returns an install snippet to give to a coding agent. By default the MCP is public and NO consumer token is minted: pass initial_consumer_label to mint one, or call mcp_mint_consumer_token afterwards. Do not tell the user they have a token unless raw_consumer_token came back non-null. The upstream secret is encrypted at rest with a Fernet key; it is never returned to clients after this call.", {
|
|
1521
1710
|
name: z.string().describe("Human-readable name, e.g. 'Acme Auth MCP'"),
|
|
1522
1711
|
upstream_base_url: z.string().describe("Base URL of the business's real backend. Do not use PREMAN_BACKEND / api.preman.live unless deploying an MCP for PreMan itself."),
|
|
1523
1712
|
spec: z.any().describe("The spec_preview object returned by mcp_preview"),
|
|
1524
1713
|
initial_upstream_secret: z.string().describe("API key / bearer / basic value the MCP will use to reach the upstream backend"),
|
|
1525
1714
|
upstream_auth_style: z.any().optional().describe("Override how the credential is injected. Default: { type:'header', name:'Authorization', prefix:'Bearer ' }"),
|
|
1526
1715
|
initial_upstream_secret_type: z.string().optional().describe("bearer | api_key | basic | custom"),
|
|
1527
|
-
initial_consumer_label: z.string().optional().describe("
|
|
1716
|
+
initial_consumer_label: z.string().optional().describe("Label for the first consumer token. Omit and no token is minted and the MCP is reachable by anyone holding the URL; pass a label to mint one and get a private, revocable install snippet."),
|
|
1528
1717
|
}, async (args) => {
|
|
1529
1718
|
try {
|
|
1530
1719
|
const deployArgs = { ...args };
|
|
@@ -1617,7 +1806,7 @@ export function createServer() {
|
|
|
1617
1806
|
}
|
|
1618
1807
|
});
|
|
1619
1808
|
// ── preman_open_fix_pr ────────────────────────────────────────────
|
|
1620
|
-
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
|
|
1809
|
+
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 automatically — it merges only its own preman/fix-* branches, only on repositories that turned merging on, and only after you approve.", {
|
|
1621
1810
|
fix_task_id: z.string().describe("The id from a preman_get_fix_task result"),
|
|
1622
1811
|
branch: z.string().describe("The branch you pushed — must match package.auto_pr.branch"),
|
|
1623
1812
|
summary: z.string().optional().default("").describe("Short description of the fix, included in the PR body"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
|
|
16
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-desktop-session.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-checkout.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.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-link.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
19
|
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|