@yawlabs/lemonsqueezy-mcp 0.12.0 → 0.13.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 +9 -1
- package/bin/lemonsqueezy-mcp.mjs +97 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -318,7 +318,15 @@ npm run check:containerfile # CI runs this; non-zero exit means the two have dr
|
|
|
318
318
|
|
|
319
319
|
## Running on oam.js (optional)
|
|
320
320
|
|
|
321
|
-
[oam.js](https://oamjs.org) runs this server unmodified. Verified against oam 0.
|
|
321
|
+
[oam.js](https://oamjs.org) runs this server unmodified. Verified against oam 0.9.0: full MCP handshake, all 64 tools, the `lemonsqueezy://audit-log` resource, working `fetch`, and guardrail rejections with error text identical to Node.
|
|
322
|
+
|
|
323
|
+
**oam 0.9.0 is the minimum.** Older releases ran `child_process.execFile` arguments through a shell, which was reachable here whenever `LEMONSQUEEZY_API_KEY_COMMAND` is configured -- that feature shells out to fetch the key, and its arguments were re-split by a shell. The launcher enforces the floor: given an older oam it falls back to Node and says so on stderr, and `LEMONSQUEEZY_MCP_RUNTIME=oam` turns that into a hard error.
|
|
324
|
+
|
|
325
|
+
### Sandboxing (opt-in)
|
|
326
|
+
|
|
327
|
+
Set `LEMONSQUEEZY_MCP_SANDBOX=1` to run under oam's `--permission` model: network restricted to `api.lemonsqueezy.com` (plus the host of `LEMONSQUEEZY_SINK_URL` when set), filesystem denied outright, and child-process denied unless `LEMONSQUEEZY_API_KEY_COMMAND` is configured.
|
|
328
|
+
|
|
329
|
+
It is opt-in rather than default because a wrong grant does not fail loudly. oam denies a non-granted environment variable by making it **absent** from `process.env` rather than throwing, so an under-granted `LEMONSQUEEZY_API_KEY` reads as "unauthenticated" rather than "denied". The env allow-list in the launcher is derived from what the shipped bundle actually reads -- if you add a new `process.env` lookup, extend that list with it.
|
|
322
330
|
|
|
323
331
|
```jsonc
|
|
324
332
|
{
|
package/bin/lemonsqueezy-mcp.mjs
CHANGED
|
@@ -22,18 +22,48 @@
|
|
|
22
22
|
* For an MCP host config, point straight at oam and skip this file:
|
|
23
23
|
* { "command": "oam", "args": ["run", "<abs>/dist/index.js"] }
|
|
24
24
|
*
|
|
25
|
+
* THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
|
|
26
|
+
* `LEMONSQUEEZY_MCP_SANDBOX=1` runs the server under oam's permission model:
|
|
27
|
+
* network limited to api.lemonsqueezy.com, filesystem denied outright.
|
|
28
|
+
*
|
|
29
|
+
* LEMONSQUEEZY_SINK_URL is operator-configured, so its host is parsed out and
|
|
30
|
+
* added to the grant when set rather than assumed. Child-process stays denied
|
|
31
|
+
* UNLESS LEMONSQUEEZY_API_KEY_COMMAND is configured -- that feature shells out
|
|
32
|
+
* to fetch the key, so the grant is tied to the feature instead of handed over
|
|
33
|
+
* unconditionally.
|
|
34
|
+
*
|
|
35
|
+
* Opt-in, not default, because a wrong grant does not fail loudly. oam denies a
|
|
36
|
+
* non-granted environment variable by making it ABSENT from process.env rather
|
|
37
|
+
* than throwing, so an under-granted LEMONSQUEEZY_API_KEY reads as
|
|
38
|
+
* "unauthenticated" rather than "denied". The env list is derived from what the
|
|
39
|
+
* shipped bundle actually reads -- keep it in step with the bundle.
|
|
40
|
+
*
|
|
41
|
+
* MINIMUM OAM VERSION
|
|
42
|
+
* 0.9.0. Below it `child_process.execFile` ran its arguments through a SHELL,
|
|
43
|
+
* `exec` accepted `timeout` and ignored it, `spawnSync` truncated at
|
|
44
|
+
* `maxBuffer` while reporting success, and `stdio: 'inherit'`/`'ignore'` both
|
|
45
|
+
* behaved as `'pipe'`. This server shells out only when
|
|
46
|
+
* LEMONSQUEEZY_API_KEY_COMMAND is configured, so the execFile bug was reachable
|
|
47
|
+
* on exactly that path -- the command's arguments were re-split by a shell.
|
|
48
|
+
* An older oam is not an error: the launcher falls back to Node and says so on
|
|
49
|
+
* stderr. Pinning the floor here is what makes that fallback automatic.
|
|
50
|
+
*
|
|
25
51
|
* SELECTION
|
|
26
52
|
* LEMONSQUEEZY_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
27
53
|
* LEMONSQUEEZY_MCP_RUNTIME=node never use oam
|
|
28
54
|
* LEMONSQUEEZY_MCP_RUNTIME=auto prefer oam, silently fall back (default)
|
|
55
|
+
* LEMONSQUEEZY_MCP_SANDBOX=1 run oam under --permission (oam 0.9.0+)
|
|
29
56
|
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
|
|
30
57
|
*/
|
|
31
58
|
|
|
32
|
-
import { spawn } from "node:child_process";
|
|
59
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
33
60
|
import { existsSync } from "node:fs";
|
|
34
61
|
import { constants, homedir } from "node:os";
|
|
35
62
|
import { delimiter, join } from "node:path";
|
|
36
|
-
import { fileURLToPath } from "node:url";
|
|
63
|
+
import { fileURLToPath } from "node:url";
|
|
64
|
+
|
|
65
|
+
/** Oldest oam whose `child_process` matches Node. See MINIMUM OAM VERSION above. */
|
|
66
|
+
const OAM_MIN = [0, 9, 0];
|
|
37
67
|
|
|
38
68
|
// Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
|
|
39
69
|
// with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
|
|
@@ -83,6 +113,70 @@ function findOam() {
|
|
|
83
113
|
return null;
|
|
84
114
|
}
|
|
85
115
|
|
|
116
|
+
/**
|
|
117
|
+
* `oam --version` -> [major, minor, patch], or null when it cannot be read.
|
|
118
|
+
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
119
|
+
*/
|
|
120
|
+
function oamVersion(cmd) {
|
|
121
|
+
try {
|
|
122
|
+
const out = execFileSync(cmd, ["--version"], {
|
|
123
|
+
encoding: "utf-8",
|
|
124
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
125
|
+
});
|
|
126
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
|
|
127
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
128
|
+
} catch {
|
|
129
|
+
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True when `v` is at least `min`, comparing major/minor/patch in order. */
|
|
135
|
+
function atLeast(v, min) {
|
|
136
|
+
if (!v) return false;
|
|
137
|
+
for (let i = 0; i < min.length; i++) {
|
|
138
|
+
if (v[i] > min[i]) return true;
|
|
139
|
+
if (v[i] < min[i]) return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The `--permission` grant list, or [] when the sandbox is not requested.
|
|
146
|
+
*
|
|
147
|
+
* These are oam's PROCESS-level flags: they belong before the `run` subcommand,
|
|
148
|
+
* not after it. `oam run --permission file.js` is rejected outright, which is a
|
|
149
|
+
* good failure but only because it is loud -- ordering here is load-bearing.
|
|
150
|
+
*
|
|
151
|
+
* Net grants prefix-match `host` for fetch and `host:port` for sockets.
|
|
152
|
+
* A denied environment variable is ABSENT from process.env rather than throwing,
|
|
153
|
+
* so the env list below is derived from what the bundle actually reads; trimming
|
|
154
|
+
* it produces silent misbehaviour, not a clear denial.
|
|
155
|
+
*/
|
|
156
|
+
function sandboxFlags() {
|
|
157
|
+
if (process.env.LEMONSQUEEZY_MCP_SANDBOX !== "1") return [];
|
|
158
|
+
|
|
159
|
+
const hosts = ["api.lemonsqueezy.com"];
|
|
160
|
+
// LEMONSQUEEZY_SINK_URL is operator-configured, so its host has to be learned, not assumed.
|
|
161
|
+
if (process.env.LEMONSQUEEZY_SINK_URL) {
|
|
162
|
+
try {
|
|
163
|
+
const { hostname } = new URL(process.env.LEMONSQUEEZY_SINK_URL);
|
|
164
|
+
if (hostname && !hosts.includes(hostname)) hosts.push(hostname);
|
|
165
|
+
} catch {
|
|
166
|
+
// Malformed URL: the feature is already broken on its own terms; adding
|
|
167
|
+
// nothing here keeps the grant honest rather than guessing a host.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const netFlag = `--allow-net=${hosts.join(",")}`;
|
|
171
|
+
|
|
172
|
+
const env = ["LEMONSQUEEZY_ALLOWED_STORE_IDS","LEMONSQUEEZY_API_KEY","LEMONSQUEEZY_API_KEY_COMMAND","LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT","LEMONSQUEEZY_DISABLE_CLASSES","LEMONSQUEEZY_LOG","LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS","LEMONSQUEEZY_RATE_LIMIT_PER_CLASS","LEMONSQUEEZY_SINK_ADMIN_TOKEN","LEMONSQUEEZY_SINK_URL","PATH"];
|
|
173
|
+
|
|
174
|
+
const flags = ["--permission", netFlag, `--allow-env=${env.join(",")}`];
|
|
175
|
+
// Tied to the feature that needs it, not granted unconditionally.
|
|
176
|
+
if (process.env.LEMONSQUEEZY_API_KEY_COMMAND) flags.push("--allow-child-process");
|
|
177
|
+
return flags;
|
|
178
|
+
}
|
|
179
|
+
|
|
86
180
|
/** Run the server in THIS process. The zero-overhead fallback. */
|
|
87
181
|
async function runInProcess() {
|
|
88
182
|
// A server may gate its bootstrap on being the process ENTRY POINT --
|
|
@@ -123,7 +217,7 @@ if (mode === "node") {
|
|
|
123
217
|
} else {
|
|
124
218
|
// `--` separates oam's own flags from the script's argv, so `lemonsqueezy-mcp
|
|
125
219
|
// --version` and any host-supplied flags survive the hop unchanged.
|
|
126
|
-
const child = spawn(oam, ["run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
220
|
+
const child = spawn(oam, [...sandboxFlags(), "run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
127
221
|
// inherit keeps the SAME fds, so MCP's newline-delimited JSON framing on
|
|
128
222
|
// stdin/stdout is untouched and the host's stdin-close still reaches the
|
|
129
223
|
// server's shutdown path.
|
package/dist/index.js
CHANGED
|
@@ -23793,7 +23793,7 @@ function readAuditLogResource(uri) {
|
|
|
23793
23793
|
}
|
|
23794
23794
|
|
|
23795
23795
|
// src/index.ts
|
|
23796
|
-
var version2 = true ? "0.
|
|
23796
|
+
var version2 = true ? "0.13.0" : createRequire(import.meta.url)("../package.json").version;
|
|
23797
23797
|
var subcommand = process.argv[2];
|
|
23798
23798
|
if (subcommand === "version" || subcommand === "--version") {
|
|
23799
23799
|
console.log(version2);
|
package/package.json
CHANGED