premanmcp 0.4.0 → 0.7.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 CHANGED
@@ -4,26 +4,33 @@ 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
- ## Install
7
+ ## Connect
8
8
 
9
9
  ```bash
10
- npm exec -y premanmcp@latest -- install
10
+ npm exec -y premanmcp@latest -- connect
11
11
  ```
12
12
 
13
+ Pick your coding agent from the list — Cursor, Claude Code, or Codex — and PreMan
14
+ writes that agent's MCP config for you. No hand-edited config anywhere.
15
+
13
16
  Local development form:
14
17
 
15
18
  ```bash
16
- node bin/cli.js install
19
+ node bin/cli.js connect
17
20
  ```
18
21
 
19
- First-time users are prompted for email, OTP, and password directly in the terminal. PreMan creates or connects the account, generates an API key, saves it to `~/.preman/credentials.json`, then writes a `preman` MCP server into `~/.cursor/mcp.json`:
22
+ First-time users are prompted for email, OTP, and password directly in the terminal.
23
+ PreMan creates or connects the account, generates an API key, saves it to
24
+ `~/.preman/credentials.json`, then writes a `preman` MCP server into the config your
25
+ agent actually reads (`~/.cursor/mcp.json`, Claude Code's MCP config, or
26
+ `~/.codex/config.toml`):
20
27
 
21
28
  ```json
22
29
  {
23
30
  "mcpServers": {
24
31
  "preman": {
25
32
  "command": "npm",
26
- "args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
33
+ "args": ["exec", "-y", "premanmcp@latest", "--"],
27
34
  "env": {
28
35
  "PREMAN_BACKEND": "https://api.preman.live",
29
36
  "PREMAN_FRONTEND": "https://app.preman.live"
@@ -33,9 +40,34 @@ First-time users are prompted for email, OTP, and password directly in the termi
33
40
  }
34
41
  ```
35
42
 
36
- Restart Cursor or toggle the MCP server off/on after install. The MCP server loads the saved key automatically.
43
+ Restart your agent afterwards, then ask it to `run preman_status` to finish linking.
44
+
45
+ `connect` reads the config back after writing it, and prints a copy-paste snippet if it
46
+ cannot confirm the entry landed. It then waits for your agent to check in — that
47
+ check-in is the only real proof the agent loaded the server.
48
+
49
+ Once the agent has checked in, `connect` walks you into your first test: it runs one
50
+ against an endpoint you already have, or prints the discovery brief to hand your agent,
51
+ followed by `preman endpoints setup` and `preman test`. Pass `--no-guide` to skip it.
52
+
53
+ Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
54
+ project-local config, `--print` shows the config without writing it, `--no-guide` skips
55
+ the guided first run.
56
+
57
+ In CI or any non-interactive shell, run `connect --agent <name> --api-key pm_live_…`.
58
+ Without `--agent` there is nothing to prompt on, so `connect` prints ready-to-paste
59
+ setup blocks for all three agents and exits 2.
60
+
61
+ `preman install` still exists and does the Cursor-only half of this.
37
62
 
38
- You can also create or connect your account before install:
63
+ ### Cloud dispatch (optional)
64
+
65
+ `connect` offers to store a coding-agent credential — a Cursor API key, or a Claude
66
+ Code routine token and id. With one saved, PreMan can start an agent run for you when
67
+ it finds a failing endpoint instead of handing back a prompt to paste. Press Enter to
68
+ skip; everything else still works.
69
+
70
+ You can also create or connect your account first:
39
71
 
40
72
  ```bash
41
73
  npm exec -y premanmcp@latest -- login
@@ -44,13 +76,13 @@ npm exec -y premanmcp@latest -- login
44
76
  You can also pass the key directly:
45
77
 
46
78
  ```bash
47
- npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
79
+ npm exec -y premanmcp@latest -- connect --api-key pm_live_xxx
48
80
  ```
49
81
 
50
- For project-local installation:
82
+ For project-local config:
51
83
 
52
84
  ```bash
53
- npm exec -y premanmcp@latest -- install --project
85
+ npm exec -y premanmcp@latest -- connect --project
54
86
  ```
55
87
 
56
88
  ## What It Does
@@ -63,6 +95,7 @@ npm exec -y premanmcp@latest -- install --project
63
95
  - Supports hosted MCPs with consumer tokens for customer-facing agent access.
64
96
  - Records per-call observability so teams can audit which agent did what.
65
97
  - Hands failing-endpoint alerts to your agent as fix tasks (`preman_get_fix_task` → repro curl → `preman_complete_fix_task`).
98
+ - Connects your production logs from the terminal (`connect_logs`): your agent asks where the logs live, then deploys a read-only CloudFormation role for AWS, or wires a shipper to the ingest endpoint for everything else.
66
99
 
67
100
  ## Common Agent Commands
68
101
 
@@ -92,22 +125,24 @@ Show me the audit log for this hosted MCP.
92
125
  Pull my pending PreMan fix tasks and fix the failing endpoint.
93
126
  ```
94
127
 
95
- ## Cursor
128
+ ```text
129
+ Connect my production logs to PreMan.
130
+ ```
96
131
 
97
- The installer targets Cursor by default:
132
+ ## Cursor
98
133
 
99
134
  ```bash
100
- npm exec -y premanmcp@latest -- install
135
+ npm exec -y premanmcp@latest -- connect --agent cursor
101
136
  ```
102
137
 
103
- Manual Cursor config:
138
+ Manual Cursor config, if you would rather write it yourself:
104
139
 
105
140
  ```json
106
141
  {
107
142
  "mcpServers": {
108
143
  "preman": {
109
144
  "command": "npm",
110
- "args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"]
145
+ "args": ["exec", "-y", "premanmcp@latest", "--"]
111
146
  }
112
147
  }
113
148
  }
@@ -116,18 +151,33 @@ Manual Cursor config:
116
151
  ## Claude Code
117
152
 
118
153
  ```bash
119
- claude mcp add preman -- npm exec -y --package premanmcp@latest -- premanmcp
154
+ npm exec -y premanmcp@latest -- connect --agent claude-code
155
+ ```
156
+
157
+ Equivalent manual command:
158
+
159
+ ```bash
160
+ claude mcp add preman -- npm exec -y premanmcp@latest --
120
161
  ```
121
162
 
163
+ ## Codex
164
+
165
+ ```bash
166
+ npm exec -y premanmcp@latest -- connect --agent codex
167
+ ```
168
+
169
+ Writes an `[mcp_servers.preman]` block into `~/.codex/config.toml`.
170
+
122
171
  ## CLI
123
172
 
124
173
  ```bash
125
- npm exec -y premanmcp@latest -- # Start the MCP server
126
- npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
127
- npm exec -y premanmcp@latest -- install # Install Cursor MCP config
128
- npm exec -y premanmcp@latest -- install --project # Write .cursor/mcp.json in current repo
129
- npm exec -y premanmcp@latest -- install --print # Print config without writing
130
- npm exec -y premanmcp@latest -- install --api-key KEY # Install with your PreMan API key
174
+ npm exec -y premanmcp@latest -- connect # Pick an agent and connect it
175
+ npm exec -y premanmcp@latest -- connect --agent codex # Skip the picker
176
+ npm exec -y premanmcp@latest -- connect --project # Write project-local config
177
+ npm exec -y premanmcp@latest -- connect --print # Print config without writing
178
+ npm exec -y premanmcp@latest -- # Start the MCP server
179
+ npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
180
+ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
131
181
  ```
132
182
 
133
183
  Options:
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `preman endpoints` and `preman test` — the API-testing surface of the CLI.
3
+ *
4
+ * Both are thin proxies over `POST /mcp/call-tool`, exactly like the MCP
5
+ * server's tools, so the CLI and an agent calling the same tool see identical
6
+ * behaviour. Requires a pm_live_ key (flag, env, or stored credentials).
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+
11
+ import { callBackendJson, makeArgs, resolveApiKey } from "./shared.js";
12
+
13
+ export const ENDPOINTS_HELP = `
14
+ Endpoints:
15
+ preman endpoints list [--status s] [--method m] [--workbench] [--json-out]
16
+ List endpoints saved in PreMan (add --workbench for runnable requests).
17
+ preman endpoints discover [--framework hint]
18
+ Print the codebase-discovery brief for your coding agent to execute.
19
+ preman endpoints setup (--file endpoints.json | --ids id1,id2) [--base-url url] [--no-workbench]
20
+ Register discovered endpoints and set them up as runnable requests.
21
+ `;
22
+
23
+ export const TEST_HELP = `
24
+ Testing:
25
+ preman test <request-or-endpoint-id> [options]
26
+ Generate scenario tests for one endpoint and run them.
27
+ --scenario "..." Add a described scenario (repeatable)
28
+ --no-run Generate only, don't execute
29
+ --allow-writes Permit mutating cases / write endpoints
30
+ --code Emit unit-test files (prints filename + content)
31
+ --framework pytest|jest Framework for --code (default pytest)
32
+ --stress Run a bounded load test instead
33
+ --duration n --rps n --concurrency n Stress parameters
34
+ --json-out Print the raw JSON result
35
+ `;
36
+
37
+ class CliError extends Error {
38
+ constructor(message, exitCode = 1) {
39
+ super(message);
40
+ this.exitCode = exitCode;
41
+ }
42
+ }
43
+
44
+ export async function callTool(args, tool, toolArguments) {
45
+ const token = resolveApiKey(args);
46
+ if (!token) {
47
+ throw new CliError(
48
+ "No PreMan API key. Run `preman login` or pass --api-key pm_live_...",
49
+ 2,
50
+ );
51
+ }
52
+ const result = await callBackendJson(args, "POST", "/mcp/call-tool", {
53
+ json: { tool, arguments: toolArguments },
54
+ token,
55
+ });
56
+ if (!result.ok) {
57
+ const detail = result.detail || result.message || result.raw || "backend error";
58
+ throw new CliError(`${tool} failed: ${result.status_code} ${detail}`);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function printJson(value) {
64
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
65
+ }
66
+
67
+ function collectRepeated(commandArgs, flag) {
68
+ const out = [];
69
+ for (let i = 0; i < commandArgs.length; i += 1) {
70
+ if (commandArgs[i] === flag && commandArgs[i + 1]) out.push(commandArgs[i + 1]);
71
+ }
72
+ return out;
73
+ }
74
+
75
+ export async function endpointsCommand(commandArgs) {
76
+ const sub = commandArgs[0] && !commandArgs[0].startsWith("-") ? commandArgs[0] : "list";
77
+ const rest = sub === commandArgs[0] ? commandArgs.slice(1) : commandArgs;
78
+ const args = makeArgs(rest);
79
+
80
+ if (sub === "list") {
81
+ const toolArguments = {
82
+ status: args.value("--status", "") || undefined,
83
+ method: args.value("--method", "") || undefined,
84
+ include_workbench: args.has("--workbench"),
85
+ limit: Number(args.value("--limit", "50")),
86
+ };
87
+ const result = await callTool(args, "get_endpoints", toolArguments);
88
+ if (args.has("--json-out")) return printJson(result);
89
+ const rows = result.endpoints || [];
90
+ process.stdout.write(`${rows.length} registry endpoint(s)\n`);
91
+ for (const ep of rows) {
92
+ const status = ep.status ? ` [${ep.status}]` : "";
93
+ process.stdout.write(` ${ep.method || "GET"} ${ep.path_template || ep.url || ""}${status} ${ep.id || ""}\n`);
94
+ }
95
+ const workbench = result.workbench_requests || [];
96
+ if (workbench.length) {
97
+ process.stdout.write(`${workbench.length} runnable workbench request(s)\n`);
98
+ for (const r of workbench) {
99
+ process.stdout.write(` ${r.method} ${r.url} ${r.id}\n`);
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ if (sub === "discover") {
106
+ const result = await callTool(args, "discover_endpoints_from_codebase", {
107
+ base_path: args.value("--path", "."),
108
+ framework_hint: args.value("--framework", "") || undefined,
109
+ });
110
+ if (args.has("--json-out")) return printJson(result);
111
+ for (const line of result.instructions || []) process.stdout.write(`${line}\n`);
112
+ process.stdout.write(
113
+ "\nHand this brief to your coding agent, then run `preman endpoints setup --file endpoints.json`.\n",
114
+ );
115
+ return undefined;
116
+ }
117
+
118
+ if (sub === "setup") {
119
+ const file = args.value("--file", "");
120
+ const ids = args.value("--ids", "");
121
+ const toolArguments = {
122
+ base_url: args.value("--base-url", "") || undefined,
123
+ setup_workbench: !args.has("--no-workbench"),
124
+ };
125
+ if (file) {
126
+ let parsed;
127
+ try {
128
+ parsed = JSON.parse(readFileSync(file, "utf8"));
129
+ } catch (error) {
130
+ throw new CliError(`could not read ${file}: ${error.message}`, 2);
131
+ }
132
+ toolArguments.endpoints = Array.isArray(parsed) ? parsed : parsed.endpoints;
133
+ } else if (ids) {
134
+ toolArguments.endpoint_ids = ids.split(",").map((s) => s.trim()).filter(Boolean);
135
+ } else {
136
+ throw new CliError("pass --file endpoints.json or --ids id1,id2", 2);
137
+ }
138
+ const result = await callTool(args, "register_discovered_endpoints", toolArguments);
139
+ if (args.has("--json-out")) return printJson(result);
140
+ process.stdout.write(
141
+ `registered ${result.registered || 0} endpoint(s), skipped ${result.skipped || 0}\n`,
142
+ );
143
+ const requests = result.workbench_request_ids || [];
144
+ if (requests.length) {
145
+ process.stdout.write(`runnable as: ${requests.join(", ")}\n`);
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ throw new CliError(`unknown endpoints subcommand: ${sub}${ENDPOINTS_HELP}`, 2);
151
+ }
152
+
153
+ /** Print the scenario list and run outcome from generate_endpoint_tests. */
154
+ export function printTestSummary(result) {
155
+ process.stdout.write(`${result.count || 0} scenario(s) for ${result.request_id}\n`);
156
+ for (const c of result.cases || []) {
157
+ process.stdout.write(` - ${c.name} (${c.kind})\n`);
158
+ }
159
+ const run = result.run;
160
+ if (!run) return;
161
+ process.stdout.write(
162
+ `run ${run.status}: ${run.passed}/${run.total} passed, ${run.skipped} skipped` +
163
+ `${run.aborted_reason ? `, aborted: ${run.aborted_reason}` : ""}\n`,
164
+ );
165
+ for (const o of run.outcomes || []) {
166
+ if (o.status !== "passed") {
167
+ process.stdout.write(` FAIL ${o.case_name}: ${o.response_status ?? o.error ?? ""}\n`);
168
+ }
169
+ }
170
+ }
171
+
172
+ export async function testCommand(commandArgs) {
173
+ const target = commandArgs[0] && !commandArgs[0].startsWith("-") ? commandArgs[0] : "";
174
+ const rest = target ? commandArgs.slice(1) : commandArgs;
175
+ const args = makeArgs(rest);
176
+ if (!target) {
177
+ throw new CliError(`pass a request or endpoint id${TEST_HELP}`, 2);
178
+ }
179
+
180
+ if (args.has("--stress")) {
181
+ const result = await callTool(args, "run_stress_test", {
182
+ target,
183
+ duration_seconds: Number(args.value("--duration", "15")),
184
+ rps: Number(args.value("--rps", "5")),
185
+ concurrency: Number(args.value("--concurrency", "5")),
186
+ allow_writes: args.has("--allow-writes"),
187
+ });
188
+ if (args.has("--json-out")) return printJson(result);
189
+ process.stdout.write(
190
+ `stress ${result.status}: ${result.total_requests} requests, ` +
191
+ `${Math.round((result.error_rate || 0) * 1000) / 10}% errors, ` +
192
+ `p50 ${result.p50_ms ?? "n/a"}ms p95 ${result.p95_ms ?? "n/a"}ms p99 ${result.p99_ms ?? "n/a"}ms, ` +
193
+ `${Math.round((result.throughput_rps || 0) * 10) / 10} rps\n`,
194
+ );
195
+ if (result.aborted_reason) process.stdout.write(`aborted: ${result.aborted_reason}\n`);
196
+ return undefined;
197
+ }
198
+
199
+ const result = await callTool(args, "generate_endpoint_tests", {
200
+ target,
201
+ scenarios: collectRepeated(rest, "--scenario"),
202
+ run: !args.has("--no-run"),
203
+ allow_writes: args.has("--allow-writes"),
204
+ max_cases: Number(args.value("--max-cases", "10")),
205
+ include_code: args.has("--code"),
206
+ test_framework: args.value("--framework", "pytest"),
207
+ });
208
+ if (args.has("--json-out")) return printJson(result);
209
+
210
+ printTestSummary(result);
211
+ for (const artifact of result.code_artifacts || []) {
212
+ process.stdout.write(`\n===== ${artifact.filename} =====\n${artifact.content}\n`);
213
+ }
214
+ return undefined;
215
+ }