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 +72 -22
- package/bin/api_tools.js +215 -0
- package/bin/cli.js +63 -324
- package/bin/connect.js +715 -0
- package/bin/integrations.js +367 -0
- package/bin/shared.js +351 -0
- package/dist/server.js +121 -1
- package/package.json +3 -2
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
|
-
##
|
|
7
|
+
## Connect
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm exec -y premanmcp@latest --
|
|
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
|
|
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.
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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 --
|
|
79
|
+
npm exec -y premanmcp@latest -- connect --api-key pm_live_xxx
|
|
48
80
|
```
|
|
49
81
|
|
|
50
|
-
For project-local
|
|
82
|
+
For project-local config:
|
|
51
83
|
|
|
52
84
|
```bash
|
|
53
|
-
npm exec -y premanmcp@latest --
|
|
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
|
-
|
|
128
|
+
```text
|
|
129
|
+
Connect my production logs to PreMan.
|
|
130
|
+
```
|
|
96
131
|
|
|
97
|
-
|
|
132
|
+
## Cursor
|
|
98
133
|
|
|
99
134
|
```bash
|
|
100
|
-
npm exec -y premanmcp@latest --
|
|
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", "
|
|
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
|
-
|
|
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 --
|
|
126
|
-
npm exec -y premanmcp@latest --
|
|
127
|
-
npm exec -y premanmcp@latest --
|
|
128
|
-
npm exec -y premanmcp@latest --
|
|
129
|
-
npm exec -y premanmcp@latest --
|
|
130
|
-
npm exec -y premanmcp@latest --
|
|
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:
|
package/bin/api_tools.js
ADDED
|
@@ -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
|
+
}
|