@postmcp/cli 0.1.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/bin.d.mts +5 -0
- package/dist/bin.d.ts +5 -0
- package/dist/bin.js +831 -0
- package/dist/bin.js.map +1 -0
- package/dist/bin.mjs +803 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.d.mts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +828 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +772 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/commands/run.ts
|
|
10
|
+
import { parseOpenAPI, startStdioServer, startHttpServer } from "@postmcp/core";
|
|
11
|
+
|
|
12
|
+
// src/config/loader.ts
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
function loadEnvFile(envFilePath) {
|
|
17
|
+
if (envFilePath) {
|
|
18
|
+
const resolved = path.resolve(envFilePath);
|
|
19
|
+
if (fs.existsSync(resolved)) {
|
|
20
|
+
dotenv.config({ path: resolved });
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
const defaultEnv = path.resolve(process.cwd(), ".env");
|
|
24
|
+
if (fs.existsSync(defaultEnv)) {
|
|
25
|
+
dotenv.config({ path: defaultEnv });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function loadConfigFile(configPath) {
|
|
30
|
+
let targetPath = configPath ? path.resolve(configPath) : null;
|
|
31
|
+
if (!targetPath) {
|
|
32
|
+
const candidates = ["postmcp.config.json", "postmcp.json", ".postmcprc.json"];
|
|
33
|
+
for (const c of candidates) {
|
|
34
|
+
const candidatePath = path.resolve(process.cwd(), c);
|
|
35
|
+
if (fs.existsSync(candidatePath)) {
|
|
36
|
+
targetPath = candidatePath;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (targetPath && fs.existsSync(targetPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const content = fs.readFileSync(targetPath, "utf-8");
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
function parseHeaderFlags(headers) {
|
|
52
|
+
if (!headers || !Array.isArray(headers)) return {};
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const h of headers) {
|
|
55
|
+
const colonIdx = h.indexOf(":");
|
|
56
|
+
if (colonIdx !== -1) {
|
|
57
|
+
const key = h.slice(0, colonIdx).trim();
|
|
58
|
+
const val = h.slice(colonIdx + 1).trim();
|
|
59
|
+
if (key && val) {
|
|
60
|
+
result[key] = val;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function parseApiKeyFlag(apiKeyStr) {
|
|
67
|
+
if (!apiKeyStr) return void 0;
|
|
68
|
+
let location = "header";
|
|
69
|
+
let rest = apiKeyStr;
|
|
70
|
+
if (apiKeyStr.startsWith("query:")) {
|
|
71
|
+
location = "query";
|
|
72
|
+
rest = apiKeyStr.slice(6);
|
|
73
|
+
} else if (apiKeyStr.startsWith("header:")) {
|
|
74
|
+
location = "header";
|
|
75
|
+
rest = apiKeyStr.slice(7);
|
|
76
|
+
} else if (apiKeyStr.startsWith("cookie:")) {
|
|
77
|
+
location = "cookie";
|
|
78
|
+
rest = apiKeyStr.slice(7);
|
|
79
|
+
}
|
|
80
|
+
const eqIdx = rest.indexOf("=");
|
|
81
|
+
if (eqIdx !== -1) {
|
|
82
|
+
const name = rest.slice(0, eqIdx).trim();
|
|
83
|
+
const value = rest.slice(eqIdx + 1).trim();
|
|
84
|
+
if (name && value) {
|
|
85
|
+
return { name, value, in: location };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/presets/index.ts
|
|
92
|
+
import * as fs2 from "fs";
|
|
93
|
+
import * as path2 from "path";
|
|
94
|
+
import * as os from "os";
|
|
95
|
+
import axios from "axios";
|
|
96
|
+
import { ALL_PRESETS, PRESETS_BY_ID, getPreset, buildPresetAuthConfig, Preset } from "@postmcp/presets";
|
|
97
|
+
var BUNDLED_PRESETS = PRESETS_BY_ID;
|
|
98
|
+
function getPresetCacheDir() {
|
|
99
|
+
try {
|
|
100
|
+
const dir = path2.join(os.homedir(), ".postmcp", "presets");
|
|
101
|
+
if (!fs2.existsSync(dir)) {
|
|
102
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
return dir;
|
|
105
|
+
} catch {
|
|
106
|
+
const fallbackDir = path2.join(os.tmpdir(), "postmcp-presets");
|
|
107
|
+
if (!fs2.existsSync(fallbackDir)) {
|
|
108
|
+
try {
|
|
109
|
+
fs2.mkdirSync(fallbackDir, { recursive: true });
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return fallbackDir;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function resolvePresetSpec(presetIdOrAlias) {
|
|
117
|
+
const cleanId = presetIdOrAlias.replace(/^@/, "").toLowerCase().trim();
|
|
118
|
+
const preset = getPreset(cleanId);
|
|
119
|
+
if (!preset) {
|
|
120
|
+
throw new Error(`Unknown preset '@${cleanId}'. Run 'postmcp presets list' to see all available presets.`);
|
|
121
|
+
}
|
|
122
|
+
const cacheDir = getPresetCacheDir();
|
|
123
|
+
const cacheFile = path2.join(cacheDir, `${cleanId}.json`);
|
|
124
|
+
if (fs2.existsSync(cacheFile)) {
|
|
125
|
+
return cacheFile;
|
|
126
|
+
}
|
|
127
|
+
if (preset.specUrl) {
|
|
128
|
+
try {
|
|
129
|
+
const res = await axios.get(preset.specUrl, { timeout: 8e3, responseType: "text" });
|
|
130
|
+
try {
|
|
131
|
+
fs2.writeFileSync(cacheFile, res.data, "utf-8");
|
|
132
|
+
return cacheFile;
|
|
133
|
+
} catch {
|
|
134
|
+
return res.data;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (preset.bundledSpec) {
|
|
140
|
+
try {
|
|
141
|
+
fs2.writeFileSync(cacheFile, JSON.stringify(preset.bundledSpec, null, 2), "utf-8");
|
|
142
|
+
return cacheFile;
|
|
143
|
+
} catch {
|
|
144
|
+
return preset.bundledSpec;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (preset.specUrl) {
|
|
148
|
+
return preset.specUrl;
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Preset '@${cleanId}' has no specification available.`);
|
|
151
|
+
}
|
|
152
|
+
async function syncAllPresets() {
|
|
153
|
+
const synced = [];
|
|
154
|
+
const cacheDir = getPresetCacheDir();
|
|
155
|
+
for (const preset of ALL_PRESETS) {
|
|
156
|
+
const filePath = path2.join(cacheDir, `${preset.id}.json`);
|
|
157
|
+
let wrote = false;
|
|
158
|
+
if (preset.specUrl) {
|
|
159
|
+
try {
|
|
160
|
+
const res = await axios.get(preset.specUrl, { timeout: 8e3, responseType: "text" });
|
|
161
|
+
fs2.writeFileSync(filePath, res.data, "utf-8");
|
|
162
|
+
wrote = true;
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!wrote && preset.bundledSpec) {
|
|
167
|
+
try {
|
|
168
|
+
fs2.writeFileSync(filePath, JSON.stringify(preset.bundledSpec, null, 2), "utf-8");
|
|
169
|
+
wrote = true;
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (wrote) {
|
|
174
|
+
synced.push(preset.id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return synced;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/commands/run.ts
|
|
181
|
+
import pc from "picocolors";
|
|
182
|
+
async function runCommand(specArg, options) {
|
|
183
|
+
loadEnvFile(options.envFile);
|
|
184
|
+
const fileConfig = loadConfigFile(options.config);
|
|
185
|
+
let specPath = specArg || fileConfig.spec;
|
|
186
|
+
if (!specPath) {
|
|
187
|
+
console.error(pc.red("Error: No OpenAPI spec provided. Usage: postmcp run <spec-path-or-url-or-@preset>"));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let preset = void 0;
|
|
192
|
+
if (typeof specPath === "string" && specPath.startsWith("@")) {
|
|
193
|
+
preset = getPreset(specPath);
|
|
194
|
+
try {
|
|
195
|
+
specPath = await resolvePresetSpec(specPath);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(pc.red(`Error resolving preset: ${err.message}`));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let parsedSpec;
|
|
203
|
+
try {
|
|
204
|
+
parsedSpec = await parseOpenAPI(specPath);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error(pc.red(`Failed to parse OpenAPI specification: ${err.message}`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (preset && preset.macros && preset.macros.length > 0) {
|
|
211
|
+
parsedSpec.macros = [...parsedSpec.macros || [], ...preset.macros];
|
|
212
|
+
}
|
|
213
|
+
if (fileConfig.macros && fileConfig.macros.length > 0) {
|
|
214
|
+
parsedSpec.macros = [...parsedSpec.macros || [], ...fileConfig.macros];
|
|
215
|
+
}
|
|
216
|
+
if (fileConfig.enabledOperations && Object.keys(fileConfig.enabledOperations).length > 0) {
|
|
217
|
+
parsedSpec.operations = parsedSpec.operations.filter(
|
|
218
|
+
(op) => fileConfig.enabledOperations[op.id] !== false
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const cliHeaders = parseHeaderFlags(options.header);
|
|
222
|
+
const cliApiKey = parseApiKeyFlag(options.apiKey);
|
|
223
|
+
const presetAuthConfig = preset ? buildPresetAuthConfig(preset, process.env) : {};
|
|
224
|
+
const authConfig = {
|
|
225
|
+
headers: { ...presetAuthConfig.headers, ...fileConfig.auth?.headers, ...cliHeaders },
|
|
226
|
+
bearerToken: options.bearer || fileConfig.auth?.bearerToken || presetAuthConfig.bearerToken || process.env.API_KEY || process.env.BEARER_TOKEN,
|
|
227
|
+
apiKey: cliApiKey || fileConfig.auth?.apiKey || presetAuthConfig.apiKey,
|
|
228
|
+
basicAuth: fileConfig.auth?.basicAuth || presetAuthConfig.basicAuth,
|
|
229
|
+
securitySchemes: {
|
|
230
|
+
...presetAuthConfig.securitySchemes,
|
|
231
|
+
...fileConfig.auth?.securitySchemes
|
|
232
|
+
},
|
|
233
|
+
allowedExternalHosts: fileConfig.auth?.allowedExternalHosts,
|
|
234
|
+
allowCrossOriginAuth: fileConfig.auth?.allowCrossOriginAuth
|
|
235
|
+
};
|
|
236
|
+
const transport = options.transport || fileConfig.transport || (options.port ? "http" : "stdio");
|
|
237
|
+
const resolvedBaseUrl = options.baseUrl || fileConfig.baseUrl || process.env.BASE_URL || (parsedSpec.servers.length > 0 ? parsedSpec.servers[0].url : preset?.defaultBaseUrl);
|
|
238
|
+
const isJit = options.jit !== void 0 ? options.jit : fileConfig.jit !== void 0 ? fileConfig.jit : void 0;
|
|
239
|
+
const isDryRun = options.dryRun !== void 0 ? options.dryRun : fileConfig.dryRun;
|
|
240
|
+
const isTokenDiet = options.tokenDiet !== void 0 ? options.tokenDiet : fileConfig.tokenDiet?.enabled !== false;
|
|
241
|
+
const maxTokens = options.maxTokens ? parseInt(options.maxTokens, 10) : fileConfig.tokenDiet?.maxTokens || 2500;
|
|
242
|
+
const pathFieldMasks = {};
|
|
243
|
+
if (preset?.fieldMasks) {
|
|
244
|
+
for (const fm of preset.fieldMasks) {
|
|
245
|
+
pathFieldMasks[fm.path] = fm.fields;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (fileConfig.fieldMasks) {
|
|
249
|
+
Object.assign(pathFieldMasks, fileConfig.fieldMasks);
|
|
250
|
+
}
|
|
251
|
+
const serverOptions = {
|
|
252
|
+
spec: parsedSpec,
|
|
253
|
+
baseUrl: resolvedBaseUrl,
|
|
254
|
+
auth: authConfig,
|
|
255
|
+
jit: isJit,
|
|
256
|
+
dryRun: isDryRun,
|
|
257
|
+
tokenDiet: {
|
|
258
|
+
enabled: isTokenDiet,
|
|
259
|
+
maxTokens,
|
|
260
|
+
pathFieldMasks: isTokenDiet && Object.keys(pathFieldMasks).length > 0 ? pathFieldMasks : void 0,
|
|
261
|
+
convertToMarkdownTable: fileConfig.tokenDiet?.convertToMarkdownTable !== false
|
|
262
|
+
},
|
|
263
|
+
serverName: parsedSpec.title,
|
|
264
|
+
serverVersion: parsedSpec.version
|
|
265
|
+
};
|
|
266
|
+
if (transport === "http") {
|
|
267
|
+
const port = options.port ? parseInt(options.port, 10) : fileConfig.port || 3e3;
|
|
268
|
+
const host = options.host || "localhost";
|
|
269
|
+
const { url } = await startHttpServer({
|
|
270
|
+
...serverOptions,
|
|
271
|
+
port,
|
|
272
|
+
host
|
|
273
|
+
});
|
|
274
|
+
console.log(pc.green(`PostMCP Streamable HTTP server listening at: ${pc.bold(url)}`));
|
|
275
|
+
console.log(pc.dim(` Service: ${parsedSpec.title} (v${parsedSpec.version}) | Endpoints: ${parsedSpec.operations.length}`));
|
|
276
|
+
if (isDryRun) {
|
|
277
|
+
console.log(pc.yellow(` Mode: DRY-RUN SIMULATION (Mutations simulated, no real side effects)`));
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
await startStdioServer(serverOptions);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/commands/inspect.ts
|
|
285
|
+
import { parseOpenAPI as parseOpenAPI2 } from "@postmcp/core";
|
|
286
|
+
import Table from "cli-table3";
|
|
287
|
+
import pc2 from "picocolors";
|
|
288
|
+
function estimateSpecTokenSavings(spec) {
|
|
289
|
+
const rawTokensPerOp = spec.operations.map((op) => {
|
|
290
|
+
const descLen = (op.description || op.summary || "").length;
|
|
291
|
+
const schemaLen = JSON.stringify(op.inputSchema || {}).length;
|
|
292
|
+
return Math.ceil((descLen + schemaLen + 50) / 4);
|
|
293
|
+
});
|
|
294
|
+
const totalRawToolTokens = rawTokensPerOp.reduce((a, b) => a + b, 0);
|
|
295
|
+
const avgOpTokens = spec.operations.length > 0 ? Math.ceil(totalRawToolTokens / spec.operations.length) : 0;
|
|
296
|
+
const jitPromptTokens = 120 + Math.min(spec.operations.length, 5) * Math.ceil(avgOpTokens * 0.4);
|
|
297
|
+
const raw = Math.max(totalRawToolTokens, 200);
|
|
298
|
+
const optimized = Math.min(raw, jitPromptTokens);
|
|
299
|
+
const savingsPct = Math.max(15, Math.round((raw - optimized) / raw * 100));
|
|
300
|
+
return {
|
|
301
|
+
rawTokens: raw,
|
|
302
|
+
optimizedTokens: optimized,
|
|
303
|
+
savingsPct
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function inspectCommand(specArg, options) {
|
|
307
|
+
let specPath = specArg;
|
|
308
|
+
if (!specPath) {
|
|
309
|
+
console.error(pc2.red("Error: No OpenAPI spec provided. Usage: postmcp inspect <spec-path-or-url-or-@preset>"));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let preset = void 0;
|
|
314
|
+
if (typeof specPath === "string" && specPath.startsWith("@")) {
|
|
315
|
+
preset = getPreset(specPath);
|
|
316
|
+
try {
|
|
317
|
+
specPath = await resolvePresetSpec(specPath);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(pc2.red(`Error resolving preset: ${err.message}`));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let spec;
|
|
325
|
+
try {
|
|
326
|
+
spec = await parseOpenAPI2(specPath);
|
|
327
|
+
if (preset && preset.macros && preset.macros.length > 0) {
|
|
328
|
+
spec.macros = [...spec.macros || [], ...preset.macros];
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error(pc2.red(`Failed to parse specification: ${err.message}`));
|
|
332
|
+
process.exit(1);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (options.json) {
|
|
336
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const tokenMetrics = estimateSpecTokenSavings(spec);
|
|
340
|
+
console.log();
|
|
341
|
+
console.log(pc2.bold(pc2.cyan(`PostMCP API Inspection: ${spec.title} (v${spec.version})`)));
|
|
342
|
+
if (spec.description) {
|
|
343
|
+
console.log(pc2.dim(spec.description.slice(0, 120) + (spec.description.length > 120 ? "..." : "")));
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
const overviewTable = new Table({
|
|
347
|
+
head: [pc2.bold("Metric"), pc2.bold("Value")],
|
|
348
|
+
colWidths: [25, 55]
|
|
349
|
+
});
|
|
350
|
+
const totalOps = spec.operations.length;
|
|
351
|
+
const methodsCount = {};
|
|
352
|
+
const riskCounts = { READ_ONLY: 0, MUTATION: 0, CRITICAL: 0 };
|
|
353
|
+
for (const op of spec.operations) {
|
|
354
|
+
const m = op.method.toUpperCase();
|
|
355
|
+
methodsCount[m] = (methodsCount[m] || 0) + 1;
|
|
356
|
+
riskCounts[op.riskTier] = (riskCounts[op.riskTier] || 0) + 1;
|
|
357
|
+
}
|
|
358
|
+
const methodsStr = Object.entries(methodsCount).map(([m, c]) => `${m}: ${c}`).join(" | ");
|
|
359
|
+
const defaultUrl = spec.servers.length > 0 ? spec.servers[0].url : "None declared";
|
|
360
|
+
const secSchemes = Object.keys(spec.securitySchemes).join(", ") || "None declared";
|
|
361
|
+
const recommendedJit = totalOps > 20 ? pc2.green("Yes (Dynamic JIT routing recommended)") : pc2.blue("No (Static direct tools)");
|
|
362
|
+
const savingsDisplay = pc2.green(`~${tokenMetrics.savingsPct}% (${tokenMetrics.rawTokens.toLocaleString()} \u2192 ${tokenMetrics.optimizedTokens.toLocaleString()} tokens)`);
|
|
363
|
+
overviewTable.push(
|
|
364
|
+
["Total Endpoints", `${totalOps} operations`],
|
|
365
|
+
["HTTP Methods", methodsStr],
|
|
366
|
+
["Base URL", defaultUrl],
|
|
367
|
+
["Security Schemes", secSchemes],
|
|
368
|
+
["JIT Router Mode", recommendedJit],
|
|
369
|
+
["Est. Token Savings", savingsDisplay],
|
|
370
|
+
["Macros / Workflows", `${spec.macros?.length || 0} composite macros`]
|
|
371
|
+
);
|
|
372
|
+
console.log(overviewTable.toString());
|
|
373
|
+
console.log();
|
|
374
|
+
console.log(pc2.bold("Token Diet & Context Optimization Preview:"));
|
|
375
|
+
console.log(` \u25CF Full Static Tool Declarations: ~${tokenMetrics.rawTokens.toLocaleString()} tokens`);
|
|
376
|
+
console.log(` \u25CF PostMCP JIT + Token Diet: ~${tokenMetrics.optimizedTokens.toLocaleString()} tokens`);
|
|
377
|
+
console.log(` \u25CF Context Window Savings: ${pc2.bold(pc2.green(`~${tokenMetrics.savingsPct}% saved`))}`);
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(pc2.bold("Safety & Risk Tier Breakdown:"));
|
|
380
|
+
console.log(` ${pc2.green("\u25CF READ_ONLY")}: ${riskCounts.READ_ONLY} endpoints (Safe for autonomous exploration)`);
|
|
381
|
+
console.log(` ${pc2.yellow("\u25CF MUTATION")}: ${riskCounts.MUTATION} endpoints (Creates or updates data)`);
|
|
382
|
+
console.log(` ${pc2.red("\u25CF CRITICAL")}: ${riskCounts.CRITICAL} endpoints (Destructive actions / simulated in dry-run)`);
|
|
383
|
+
console.log();
|
|
384
|
+
console.log(pc2.bold(`Operations List (Showing first 10 of ${totalOps}):`));
|
|
385
|
+
const opTable = new Table({
|
|
386
|
+
head: [pc2.bold("Tool Name (ID)"), pc2.bold("Method"), pc2.bold("Path"), pc2.bold("Risk Tier")],
|
|
387
|
+
colWidths: [22, 10, 32, 14]
|
|
388
|
+
});
|
|
389
|
+
const sampleOps = spec.operations.slice(0, 10);
|
|
390
|
+
for (const op of sampleOps) {
|
|
391
|
+
const tierColor = op.riskTier === "READ_ONLY" ? pc2.green(op.riskTier) : op.riskTier === "CRITICAL" ? pc2.red(op.riskTier) : pc2.yellow(op.riskTier);
|
|
392
|
+
opTable.push([op.id, op.method.toUpperCase(), op.path, tierColor]);
|
|
393
|
+
}
|
|
394
|
+
console.log(opTable.toString());
|
|
395
|
+
if (totalOps > 10) {
|
|
396
|
+
console.log(pc2.dim(` ... and ${totalOps - 10} more endpoints.`));
|
|
397
|
+
}
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/generate.ts
|
|
402
|
+
import * as fs3 from "fs";
|
|
403
|
+
import * as path3 from "path";
|
|
404
|
+
import { parseOpenAPI as parseOpenAPI3, generateTypeScriptProject, generatePythonProject } from "@postmcp/core";
|
|
405
|
+
import pc3 from "picocolors";
|
|
406
|
+
async function generateCommand(specArg, options) {
|
|
407
|
+
let specPath = specArg;
|
|
408
|
+
if (!specPath) {
|
|
409
|
+
console.error(pc3.red("Error: No OpenAPI spec provided. Usage: postmcp generate <spec-path-or-url-or-@preset> --target <python|typescript>"));
|
|
410
|
+
process.exit(1);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (typeof specPath === "string" && specPath.startsWith("@")) {
|
|
414
|
+
try {
|
|
415
|
+
specPath = await resolvePresetSpec(specPath);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.error(pc3.red(`Error resolving preset: ${err.message}`));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
let spec;
|
|
423
|
+
try {
|
|
424
|
+
spec = await parseOpenAPI3(specPath);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error(pc3.red(`Failed to parse specification: ${err.message}`));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const rawLang = (options.lang || options.target || "ts").toLowerCase();
|
|
431
|
+
let targetLang;
|
|
432
|
+
if (rawLang === "ts" || rawLang === "typescript") {
|
|
433
|
+
targetLang = "ts";
|
|
434
|
+
} else if (rawLang === "py" || rawLang === "python") {
|
|
435
|
+
targetLang = "py";
|
|
436
|
+
} else {
|
|
437
|
+
console.error(pc3.red(`Unsupported language '${rawLang}'. Supported languages: 'ts', 'py'.`));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const outDir = path3.resolve(options.out || `./${spec.title.toLowerCase().replace(/[^a-z0-9]/g, "-")}-mcp`);
|
|
442
|
+
console.log(pc3.cyan(`Generating standalone ${pc3.bold(targetLang.toUpperCase())} MCP server for '${spec.title}'...`));
|
|
443
|
+
const project = targetLang === "ts" ? generateTypeScriptProject(spec) : generatePythonProject(spec);
|
|
444
|
+
if (!fs3.existsSync(outDir)) {
|
|
445
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
446
|
+
}
|
|
447
|
+
for (const [relativePath, content] of Object.entries(project.files)) {
|
|
448
|
+
const filePath = path3.join(outDir, relativePath);
|
|
449
|
+
const parentDir = path3.dirname(filePath);
|
|
450
|
+
if (!fs3.existsSync(parentDir)) {
|
|
451
|
+
fs3.mkdirSync(parentDir, { recursive: true });
|
|
452
|
+
}
|
|
453
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
454
|
+
console.log(` ${pc3.green("+")} ${relativePath}`);
|
|
455
|
+
}
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(pc3.green(`Standalone MCP server successfully generated at:`));
|
|
458
|
+
console.log(` ${pc3.bold(outDir)}`);
|
|
459
|
+
console.log();
|
|
460
|
+
console.log(pc3.dim("Next steps:"));
|
|
461
|
+
if (targetLang === "ts") {
|
|
462
|
+
console.log(` cd ${path3.relative(process.cwd(), outDir) || "."}`);
|
|
463
|
+
console.log(" npm install");
|
|
464
|
+
console.log(" npm run build");
|
|
465
|
+
console.log(" npm start");
|
|
466
|
+
} else {
|
|
467
|
+
console.log(` cd ${path3.relative(process.cwd(), outDir) || "."}`);
|
|
468
|
+
console.log(" pip install -r requirements.txt");
|
|
469
|
+
console.log(" python server.py");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/export.ts
|
|
474
|
+
import * as fs4 from "fs";
|
|
475
|
+
import * as path4 from "path";
|
|
476
|
+
import * as os2 from "os";
|
|
477
|
+
import { parseOpenAPI as parseOpenAPI4 } from "@postmcp/core";
|
|
478
|
+
import pc4 from "picocolors";
|
|
479
|
+
function getClientConfigPath(client) {
|
|
480
|
+
const home = os2.homedir();
|
|
481
|
+
if (client === "cursor") {
|
|
482
|
+
return path4.join(process.cwd(), ".cursor", "mcp.json");
|
|
483
|
+
}
|
|
484
|
+
if (client === "claude") {
|
|
485
|
+
if (process.platform === "darwin") {
|
|
486
|
+
return path4.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
487
|
+
}
|
|
488
|
+
if (process.platform === "win32") {
|
|
489
|
+
return path4.join(process.env.APPDATA || path4.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
490
|
+
}
|
|
491
|
+
return path4.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
492
|
+
}
|
|
493
|
+
if (client === "windsurf") {
|
|
494
|
+
return path4.join(home, ".codeium", "windsurf", "mcp_config.json");
|
|
495
|
+
}
|
|
496
|
+
return path4.join(process.cwd(), "mcp.json");
|
|
497
|
+
}
|
|
498
|
+
function buildClientConfigSnippet(serverKey, specPath, options) {
|
|
499
|
+
const env = {};
|
|
500
|
+
if (options.bearer) {
|
|
501
|
+
env["API_KEY"] = options.bearer;
|
|
502
|
+
}
|
|
503
|
+
if (options.baseUrl) {
|
|
504
|
+
env["BASE_URL"] = options.baseUrl;
|
|
505
|
+
}
|
|
506
|
+
if (options.env) {
|
|
507
|
+
for (const e of options.env) {
|
|
508
|
+
const [k, v] = e.split("=");
|
|
509
|
+
if (k && v) env[k] = v;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
mcpServers: {
|
|
514
|
+
[serverKey]: {
|
|
515
|
+
command: "npx",
|
|
516
|
+
args: ["-y", "postmcp", "run", specPath],
|
|
517
|
+
env: Object.keys(env).length > 0 ? env : void 0
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async function exportCommand(specArg, options) {
|
|
523
|
+
let specPath = specArg;
|
|
524
|
+
if (!specPath) {
|
|
525
|
+
console.error(pc4.red("Error: No OpenAPI spec provided. Usage: postmcp export <spec-path-or-url-or-@preset> --target cursor|claude|windsurf|all"));
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
let serverKey = "api-server";
|
|
529
|
+
if (specPath.startsWith("@")) {
|
|
530
|
+
serverKey = specPath.replace(/^@/, "").toLowerCase();
|
|
531
|
+
} else {
|
|
532
|
+
try {
|
|
533
|
+
const parsed = await parseOpenAPI4(specPath);
|
|
534
|
+
serverKey = parsed.title.toLowerCase().replace(/[^a-z0-9]/g, "-") || "api-server";
|
|
535
|
+
} catch {
|
|
536
|
+
serverKey = "api-server";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const selectedTarget = (options.client || options.target || "all").toLowerCase();
|
|
540
|
+
const clientsToExport = selectedTarget === "all" ? ["cursor", "claude", "windsurf"] : [selectedTarget];
|
|
541
|
+
console.log(pc4.bold(pc4.cyan(`PostMCP 1-Click Client Configuration Exporter`)));
|
|
542
|
+
console.log();
|
|
543
|
+
for (const c of clientsToExport) {
|
|
544
|
+
const configPath = getClientConfigPath(c);
|
|
545
|
+
const snippet = buildClientConfigSnippet(serverKey, specPath, options);
|
|
546
|
+
const formattedSnippet = JSON.stringify(snippet, null, 2);
|
|
547
|
+
console.log(pc4.bold(pc4.green(`\u25B6 ${c.toUpperCase()} (${c === "cursor" ? "Project Local" : "Global Client"})`)));
|
|
548
|
+
console.log(pc4.dim(` Config path: ${configPath}`));
|
|
549
|
+
console.log();
|
|
550
|
+
console.log(pc4.gray(formattedSnippet));
|
|
551
|
+
console.log();
|
|
552
|
+
if (options.write) {
|
|
553
|
+
try {
|
|
554
|
+
let existingConfig = {};
|
|
555
|
+
if (fs4.existsSync(configPath)) {
|
|
556
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
557
|
+
existingConfig = JSON.parse(raw);
|
|
558
|
+
}
|
|
559
|
+
existingConfig.mcpServers = existingConfig.mcpServers || {};
|
|
560
|
+
existingConfig.mcpServers[serverKey] = snippet.mcpServers[serverKey];
|
|
561
|
+
const parentDir = path4.dirname(configPath);
|
|
562
|
+
if (!fs4.existsSync(parentDir)) {
|
|
563
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
564
|
+
}
|
|
565
|
+
fs4.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
|
|
566
|
+
console.log(pc4.green(` Successfully merged and written to ${configPath}`));
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.error(pc4.red(` Failed to write to ${configPath}: ${err.message}`));
|
|
569
|
+
}
|
|
570
|
+
console.log();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (!options.write) {
|
|
574
|
+
console.log(pc4.dim(`Tip: Pass '--write' to automatically install this configuration into your client settings.`));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/commands/presets.ts
|
|
579
|
+
import Table2 from "cli-table3";
|
|
580
|
+
import pc5 from "picocolors";
|
|
581
|
+
async function listPresetsCommand(categoryOrQuery) {
|
|
582
|
+
console.log();
|
|
583
|
+
console.log(pc5.bold(pc5.cyan(`PostMCP Presets Catalog (${ALL_PRESETS.length} Curated Developer APIs)`)));
|
|
584
|
+
console.log(pc5.dim(`Run any preset instantly using: postmcp run @<preset_id>`));
|
|
585
|
+
console.log();
|
|
586
|
+
let displayedPresets = ALL_PRESETS;
|
|
587
|
+
if (categoryOrQuery) {
|
|
588
|
+
const q = categoryOrQuery.toLowerCase().trim();
|
|
589
|
+
displayedPresets = ALL_PRESETS.filter((p) => {
|
|
590
|
+
return p.id.toLowerCase().includes(q) || p.name.toLowerCase().includes(q) || p.category.toLowerCase().includes(q) || p.tags && p.tags.some((t) => t.toLowerCase().includes(q));
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const table = new Table2({
|
|
594
|
+
head: [pc5.bold("Preset ID"), pc5.bold("API Name"), pc5.bold("Category"), pc5.bold("Authentication"), pc5.bold("Run Command")],
|
|
595
|
+
colWidths: [14, 26, 22, 28, 26]
|
|
596
|
+
});
|
|
597
|
+
for (const meta of displayedPresets) {
|
|
598
|
+
table.push([
|
|
599
|
+
pc5.bold(pc5.magenta(`@${meta.id}`)),
|
|
600
|
+
meta.name,
|
|
601
|
+
meta.category,
|
|
602
|
+
pc5.dim(meta.authType),
|
|
603
|
+
pc5.green(`postmcp run @${meta.id}`)
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
console.log(table.toString());
|
|
607
|
+
console.log();
|
|
608
|
+
console.log(pc5.dim(`Total Presets: ${displayedPresets.length} of ${ALL_PRESETS.length}`));
|
|
609
|
+
console.log(pc5.dim(`Run 'postmcp presets sync' to update offline cached schemas from GitHub.`));
|
|
610
|
+
console.log();
|
|
611
|
+
}
|
|
612
|
+
async function syncPresetsCommand() {
|
|
613
|
+
console.log(pc5.cyan(`Syncing latest OpenAPI schemas for all presets...`));
|
|
614
|
+
try {
|
|
615
|
+
const synced = await syncAllPresets();
|
|
616
|
+
console.log(pc5.green(`Successfully synced ${synced.length} presets to local cache (~/.postmcp/presets/):`));
|
|
617
|
+
for (const id of synced) {
|
|
618
|
+
console.log(` ${pc5.green("\u25CF")} @${id}`);
|
|
619
|
+
}
|
|
620
|
+
} catch (err) {
|
|
621
|
+
console.error(pc5.red(`Failed to sync presets: ${err.message}`));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/commands/studio.ts
|
|
626
|
+
import * as path5 from "path";
|
|
627
|
+
import * as fs5 from "fs";
|
|
628
|
+
import { createRequire } from "module";
|
|
629
|
+
import { spawn } from "child_process";
|
|
630
|
+
import open from "open";
|
|
631
|
+
import axios2 from "axios";
|
|
632
|
+
import pc6 from "picocolors";
|
|
633
|
+
function findStudioDir() {
|
|
634
|
+
try {
|
|
635
|
+
const customRequire = typeof createRequire !== "undefined" ? createRequire(__filename) : __require;
|
|
636
|
+
const pkgPath = customRequire.resolve("@postmcp/studio/package.json");
|
|
637
|
+
if (fs5.existsSync(pkgPath)) {
|
|
638
|
+
return path5.dirname(pkgPath);
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
const candidates = [
|
|
643
|
+
path5.resolve(__dirname, "..", "..", "studio"),
|
|
644
|
+
path5.resolve(__dirname, "..", "..", "..", "packages", "studio"),
|
|
645
|
+
path5.resolve(process.cwd(), "packages", "studio"),
|
|
646
|
+
path5.resolve(process.cwd(), "node_modules", "@postmcp", "studio")
|
|
647
|
+
];
|
|
648
|
+
for (const candidate of candidates) {
|
|
649
|
+
if (fs5.existsSync(path5.join(candidate, "package.json"))) {
|
|
650
|
+
return candidate;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return path5.resolve(process.cwd(), "packages", "studio");
|
|
654
|
+
}
|
|
655
|
+
async function waitForServer(url, timeoutMs = 2e4) {
|
|
656
|
+
const start = Date.now();
|
|
657
|
+
while (Date.now() - start < timeoutMs) {
|
|
658
|
+
try {
|
|
659
|
+
const res = await axios2.get(url, { timeout: 1e3, validateStatus: () => true });
|
|
660
|
+
if (res.status >= 200 && res.status < 500) {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
async function studioCommand(specArg, options = {}) {
|
|
670
|
+
const port = options.port || "3000";
|
|
671
|
+
const baseUrl = `http://localhost:${port}`;
|
|
672
|
+
const targetUrl = specArg ? `${baseUrl}?spec=${encodeURIComponent(specArg)}` : baseUrl;
|
|
673
|
+
const studioDir = findStudioDir();
|
|
674
|
+
console.log();
|
|
675
|
+
console.log(pc6.bold(pc6.cyan(`Starting PostMCP Visual Web Studio...`)));
|
|
676
|
+
console.log(` Port: ${pc6.bold(pc6.green(port))}`);
|
|
677
|
+
console.log(` Studio Dir: ${pc6.dim(studioDir)}`);
|
|
678
|
+
if (specArg) {
|
|
679
|
+
console.log(` Initial Spec: ${pc6.dim(specArg)}`);
|
|
680
|
+
}
|
|
681
|
+
console.log();
|
|
682
|
+
let child = null;
|
|
683
|
+
if (fs5.existsSync(studioDir)) {
|
|
684
|
+
const isBuilt = fs5.existsSync(path5.join(studioDir, ".next"));
|
|
685
|
+
const isPnpm = fs5.existsSync(path5.join(studioDir, "..", "..", "pnpm-lock.yaml"));
|
|
686
|
+
const command = isPnpm ? "pnpm" : "npx";
|
|
687
|
+
const args = isPnpm ? isBuilt ? ["start", "--port", port] : ["dev", "--port", port] : isBuilt ? ["next", "start", "-p", port] : ["next", "dev", "-p", port];
|
|
688
|
+
try {
|
|
689
|
+
child = spawn(command, args, {
|
|
690
|
+
cwd: studioDir,
|
|
691
|
+
stdio: "inherit",
|
|
692
|
+
shell: true,
|
|
693
|
+
env: {
|
|
694
|
+
...process.env,
|
|
695
|
+
PORT: port,
|
|
696
|
+
NEXT_PUBLIC_INITIAL_SPEC: specArg || "",
|
|
697
|
+
STUDIO_INITIAL_SPEC: specArg || "",
|
|
698
|
+
POSTMCP_WORKSPACE: process.cwd(),
|
|
699
|
+
WORKSPACE_CWD: process.cwd()
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
const cleanup = () => {
|
|
703
|
+
if (child) {
|
|
704
|
+
try {
|
|
705
|
+
child.kill("SIGINT");
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
process.on("SIGINT", cleanup);
|
|
711
|
+
process.on("SIGTERM", cleanup);
|
|
712
|
+
process.on("exit", cleanup);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
console.log(pc6.yellow(` Note: Running in detached standalone mode: ${err.message}`));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const isReady = await waitForServer(baseUrl, 15e3);
|
|
718
|
+
if (isReady) {
|
|
719
|
+
console.log(pc6.green(`PostMCP Visual Web Studio ready at: ${pc6.bold(targetUrl)}`));
|
|
720
|
+
} else {
|
|
721
|
+
console.log(pc6.dim(` Studio server starting at: ${targetUrl}`));
|
|
722
|
+
}
|
|
723
|
+
if (!options.noOpen) {
|
|
724
|
+
try {
|
|
725
|
+
await open(targetUrl);
|
|
726
|
+
console.log(pc6.dim(`Opening Web Studio in your default browser...`));
|
|
727
|
+
} catch {
|
|
728
|
+
console.log(pc6.dim(`Please open ${targetUrl} in your browser.`));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (child) {
|
|
732
|
+
await new Promise((resolve4) => {
|
|
733
|
+
child?.on("close", () => resolve4());
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/generators/typescript.ts
|
|
739
|
+
import { generateTypeScriptProject as generateTypeScriptProject2 } from "@postmcp/core";
|
|
740
|
+
|
|
741
|
+
// src/generators/python.ts
|
|
742
|
+
import { generatePythonProject as generatePythonProject2 } from "@postmcp/core";
|
|
743
|
+
export {
|
|
744
|
+
ALL_PRESETS,
|
|
745
|
+
BUNDLED_PRESETS,
|
|
746
|
+
PRESETS_BY_ID,
|
|
747
|
+
Preset,
|
|
748
|
+
buildClientConfigSnippet,
|
|
749
|
+
buildPresetAuthConfig,
|
|
750
|
+
estimateSpecTokenSavings,
|
|
751
|
+
exportCommand,
|
|
752
|
+
findStudioDir,
|
|
753
|
+
generateCommand,
|
|
754
|
+
generatePythonProject2 as generatePythonProject,
|
|
755
|
+
generateTypeScriptProject2 as generateTypeScriptProject,
|
|
756
|
+
getClientConfigPath,
|
|
757
|
+
getPreset,
|
|
758
|
+
getPresetCacheDir,
|
|
759
|
+
inspectCommand,
|
|
760
|
+
listPresetsCommand,
|
|
761
|
+
loadConfigFile,
|
|
762
|
+
loadEnvFile,
|
|
763
|
+
parseApiKeyFlag,
|
|
764
|
+
parseHeaderFlags,
|
|
765
|
+
resolvePresetSpec,
|
|
766
|
+
runCommand,
|
|
767
|
+
studioCommand,
|
|
768
|
+
syncAllPresets,
|
|
769
|
+
syncPresetsCommand,
|
|
770
|
+
waitForServer
|
|
771
|
+
};
|
|
772
|
+
//# sourceMappingURL=index.mjs.map
|