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/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 this also mints the first consumer token and returns one complete install snippet or hosted URL to give to a coding agent. The upstream secret is encrypted at rest with a Fernet key; it is never returned to clients after this call.", {
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("Optional label for the first consumer token. Defaults to default-consumer; pass empty only if you want to mint a token later."),
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: a human reviews and 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.13.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": {