@yawlabs/lemonsqueezy-mcp 0.7.0 → 0.8.1
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 +76 -4
- package/dist/index.js +355 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,37 @@ MCP server for the [LemonSqueezy](https://lemonsqueezy.com) API. Manage your sto
|
|
|
8
8
|
npx @yawlabs/lemonsqueezy-mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Or one-click install via Smithery:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y @smithery/cli install @yawlabs/lemonsqueezy-mcp --client claude
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Smithery prompts for your env vars (API key, optional guardrails) and writes the config into your client for you.
|
|
18
|
+
|
|
19
|
+
## What it looks like
|
|
20
|
+
|
|
21
|
+
Once configured, you can ask your AI assistant store-management questions in plain English and it routes them through the MCP tools:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
You: How much did we make from the "Pro Annual" plan last month?
|
|
25
|
+
Claude: [calls ls_list_subscriptions, ls_get_variant, ls_list_subscription_invoices]
|
|
26
|
+
Pro Annual brought in $14,280 across 84 active subscriptions in April.
|
|
27
|
+
Three of those were upgrades from monthly; none churned.
|
|
28
|
+
|
|
29
|
+
You: Refund order #LS-1234 in full.
|
|
30
|
+
Claude: [calls ls_get_order to fetch the total, then ls_refund_order with amount = total]
|
|
31
|
+
Refunded $99.00 against order LS-1234. The customer's card will see the
|
|
32
|
+
credit in 5-10 business days.
|
|
33
|
+
|
|
34
|
+
You: Disable license key abc-123 for the customer who reported abuse.
|
|
35
|
+
Claude: [calls ls_list_license_keys to find the ID, then ls_update_license_key with disabled: true]
|
|
36
|
+
License key disabled. Their existing activations will fail validation
|
|
37
|
+
on the next check.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Guardrails (refund cap, rate limit, store allowlist) catch the obvious mistakes before they reach LemonSqueezy. See [Configuration](#configuration) for the env vars that turn them on.
|
|
41
|
+
|
|
11
42
|
## Setup
|
|
12
43
|
|
|
13
44
|
Set your LemonSqueezy API key as an environment variable:
|
|
@@ -18,6 +49,17 @@ export LEMONSQUEEZY_API_KEY="your-api-key"
|
|
|
18
49
|
|
|
19
50
|
Get your API key from your [LemonSqueezy dashboard](https://app.lemonsqueezy.com/settings/api).
|
|
20
51
|
|
|
52
|
+
### Docker
|
|
53
|
+
|
|
54
|
+
A multi-stage `Dockerfile` is included at the repo root. The runtime image is a single bundled file on `node:20-alpine` running as the non-root `node` user, with no port exposed (stdio transport).
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
docker build -t yawlabs/lemonsqueezy-mcp .
|
|
58
|
+
docker run --rm -i -e LEMONSQUEEZY_API_KEY="your-api-key" yawlabs/lemonsqueezy-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A byte-identical `Containerfile` is also provided for Podman users.
|
|
62
|
+
|
|
21
63
|
### Claude Code
|
|
22
64
|
|
|
23
65
|
Add to your MCP config:
|
|
@@ -174,6 +216,7 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
|
|
|
174
216
|
| --- | --- |
|
|
175
217
|
| `LEMONSQUEEZY_API_KEY` | LemonSqueezy API token. |
|
|
176
218
|
| `LEMONSQUEEZY_API_KEY_COMMAND` | Command whose stdout produces the API key. Overrides `LEMONSQUEEZY_API_KEY`. Output is cached for 1 hour. Use this to pull short-lived credentials from a vault (`op read`, `gcloud secrets versions access`, etc.) without writing them to env vars. The cache is keyed by the command string, so changing it mid-process refreshes on the next request; it is also invalidated automatically on a 401/403 from the API, so a key rotated upstream takes effect on the next call without waiting for the TTL. |
|
|
219
|
+
| `LEMONSQUEEZY_TEST_API_KEY` | Optional test-mode key. When set and non-empty, it takes precedence over `LEMONSQUEEZY_API_KEY` (but not over `LEMONSQUEEZY_API_KEY_COMMAND`). On first activation per process, the server prints a one-line JSON `test_mode` notice to stderr so you can confirm test mode is engaged. Use this to point the server at a sandbox/test store without unsetting your production key. |
|
|
177
220
|
| `LEMONSQUEEZY_ALLOWED_STORE_IDS` | Comma-separated allowlist of store IDs. When set: (1) any tool whose input includes a `storeId` rejects calls to a non-allowed store; (2) tools that *accept* a `storeId` filter (e.g. `ls_list_orders`, `ls_list_subscriptions`) require it — calls without one are blocked so a missing filter cannot return data from every store the API key can see. Tools with no `storeId` field at all (e.g. `ls_refund_order`, `ls_cancel_subscription`, `ls_archive_customer`, `ls_delete_webhook`, `ls_delete_discount`, `ls_update_license_key`, `ls_list_stores`) route by their own resource ID and are **not** gated by this allowlist. For those, the only authoritative store boundary is the API key itself — pair this setting with a LemonSqueezy API key scoped to the same store(s), and pair with `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` / `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` for additional defense in depth. |
|
|
178
221
|
| `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. |
|
|
179
222
|
| `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Max destructive tool calls per 60-second rolling window. In-process limit — per MCP server instance, not global; each `npx` cold start resets the window. Counts include `ls_update_license_key` calls that set `disabled: true`, and `ls_update_subscription` calls that pause or switch plan. |
|
|
@@ -187,6 +230,14 @@ Each line: `{ts, event, tool?, method?, path?, status, latency_ms, request_id?,
|
|
|
187
230
|
|
|
188
231
|
HTTP errors include the upstream `X-Request-Id` when present, so support tickets to LemonSqueezy can reference the exact call.
|
|
189
232
|
|
|
233
|
+
## Resources
|
|
234
|
+
|
|
235
|
+
The server exposes one MCP Resource for clients that prefer structural retrieval over parsing stderr:
|
|
236
|
+
|
|
237
|
+
| URI | MIME type | Contents |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `lemonsqueezy://audit-log` | `application/x-ndjson` | The most recent destructive tool calls and outcomes (rate-limit blocks, refund-cap blocks, exceptions, successes). Bounded ring buffer of the last 1000 entries, most-recent-first, resets on server restart. Secret-shaped input fields are redacted before they reach the buffer. |
|
|
240
|
+
|
|
190
241
|
## Operating the server unattended
|
|
191
242
|
|
|
192
243
|
For unattended/agentic use against a live store, we recommend:
|
|
@@ -216,15 +267,36 @@ npm run test:integration # requires LEMONSQUEEZY_TEST_API_KEY + LEMONSQUEEZY_TE
|
|
|
216
267
|
|
|
217
268
|
## Releasing
|
|
218
269
|
|
|
219
|
-
|
|
270
|
+
Two paths from a clean checkout of `main`. Both produce the same artifact (npm publish with provenance + GitHub release).
|
|
271
|
+
|
|
272
|
+
### 1. Tag-and-let-CI (preferred)
|
|
220
273
|
|
|
221
274
|
```bash
|
|
222
|
-
|
|
275
|
+
# 1. Bump version
|
|
276
|
+
npm version X.Y.Z --no-git-tag-version
|
|
277
|
+
|
|
278
|
+
# 2. Commit
|
|
279
|
+
git add package.json && git commit -m "vX.Y.Z"
|
|
280
|
+
|
|
281
|
+
# 3. Annotated tag (lightweight tags are silently skipped by --follow-tags)
|
|
282
|
+
git tag -a vX.Y.Z -m "vX.Y.Z"
|
|
283
|
+
|
|
284
|
+
# 4. Push commit + tag
|
|
285
|
+
git push origin main --follow-tags
|
|
286
|
+
|
|
287
|
+
# 5. Confirm the Release workflow fired (not just CI on the bump commit)
|
|
288
|
+
gh run list --limit 2
|
|
223
289
|
```
|
|
224
290
|
|
|
225
|
-
The
|
|
291
|
+
The tag push triggers `.github/workflows/release.yml`, which runs `release.sh` in CI mode: lint, test, build, npm publish (with `--provenance`) using the org-level `NPM_TOKEN` secret, then GitHub release creation, then a smoke test against the published tarball. No local `npm login` needed.
|
|
292
|
+
|
|
293
|
+
### 2. Local end-to-end
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
./release.sh X.Y.Z
|
|
297
|
+
```
|
|
226
298
|
|
|
227
|
-
|
|
299
|
+
Does the same steps 1–7 on the workstation: lint, test, build, bump, commit, annotated tag, push, npm publish, GitHub release, verify. Idempotent — safe to re-run with the same version after a partial failure. Requires one-time setup:
|
|
228
300
|
|
|
229
301
|
```bash
|
|
230
302
|
npm login --auth-type=web # publisher of @yawlabs/lemonsqueezy-mcp
|
package/dist/index.js
CHANGED
|
@@ -3105,6 +3105,9 @@ var require_utils = __commonJS({
|
|
|
3105
3105
|
"use strict";
|
|
3106
3106
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3107
3107
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3108
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3109
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3110
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3108
3111
|
function stringArrayToHexStripped(input) {
|
|
3109
3112
|
let acc = "";
|
|
3110
3113
|
let code = 0;
|
|
@@ -3130,20 +3133,20 @@ var require_utils = __commonJS({
|
|
|
3130
3133
|
return acc;
|
|
3131
3134
|
}
|
|
3132
3135
|
var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
|
|
3133
|
-
function consumeIsZone(
|
|
3134
|
-
|
|
3136
|
+
function consumeIsZone(buffer2) {
|
|
3137
|
+
buffer2.length = 0;
|
|
3135
3138
|
return true;
|
|
3136
3139
|
}
|
|
3137
|
-
function consumeHextets(
|
|
3138
|
-
if (
|
|
3139
|
-
const hex3 = stringArrayToHexStripped(
|
|
3140
|
+
function consumeHextets(buffer2, address, output) {
|
|
3141
|
+
if (buffer2.length) {
|
|
3142
|
+
const hex3 = stringArrayToHexStripped(buffer2);
|
|
3140
3143
|
if (hex3 !== "") {
|
|
3141
3144
|
address.push(hex3);
|
|
3142
3145
|
} else {
|
|
3143
3146
|
output.error = true;
|
|
3144
3147
|
return false;
|
|
3145
3148
|
}
|
|
3146
|
-
|
|
3149
|
+
buffer2.length = 0;
|
|
3147
3150
|
}
|
|
3148
3151
|
return true;
|
|
3149
3152
|
}
|
|
@@ -3151,7 +3154,7 @@ var require_utils = __commonJS({
|
|
|
3151
3154
|
let tokenCount = 0;
|
|
3152
3155
|
const output = { error: false, address: "", zone: "" };
|
|
3153
3156
|
const address = [];
|
|
3154
|
-
const
|
|
3157
|
+
const buffer2 = [];
|
|
3155
3158
|
let endipv6Encountered = false;
|
|
3156
3159
|
let endIpv6 = false;
|
|
3157
3160
|
let consume = consumeHextets;
|
|
@@ -3164,7 +3167,7 @@ var require_utils = __commonJS({
|
|
|
3164
3167
|
if (endipv6Encountered === true) {
|
|
3165
3168
|
endIpv6 = true;
|
|
3166
3169
|
}
|
|
3167
|
-
if (!consume(
|
|
3170
|
+
if (!consume(buffer2, address, output)) {
|
|
3168
3171
|
break;
|
|
3169
3172
|
}
|
|
3170
3173
|
if (++tokenCount > 7) {
|
|
@@ -3177,22 +3180,22 @@ var require_utils = __commonJS({
|
|
|
3177
3180
|
address.push(":");
|
|
3178
3181
|
continue;
|
|
3179
3182
|
} else if (cursor === "%") {
|
|
3180
|
-
if (!consume(
|
|
3183
|
+
if (!consume(buffer2, address, output)) {
|
|
3181
3184
|
break;
|
|
3182
3185
|
}
|
|
3183
3186
|
consume = consumeIsZone;
|
|
3184
3187
|
} else {
|
|
3185
|
-
|
|
3188
|
+
buffer2.push(cursor);
|
|
3186
3189
|
continue;
|
|
3187
3190
|
}
|
|
3188
3191
|
}
|
|
3189
|
-
if (
|
|
3192
|
+
if (buffer2.length) {
|
|
3190
3193
|
if (consume === consumeIsZone) {
|
|
3191
|
-
output.zone =
|
|
3194
|
+
output.zone = buffer2.join("");
|
|
3192
3195
|
} else if (endIpv6) {
|
|
3193
|
-
address.push(
|
|
3196
|
+
address.push(buffer2.join(""));
|
|
3194
3197
|
} else {
|
|
3195
|
-
address.push(stringArrayToHexStripped(
|
|
3198
|
+
address.push(stringArrayToHexStripped(buffer2));
|
|
3196
3199
|
}
|
|
3197
3200
|
}
|
|
3198
3201
|
output.address = address.join("");
|
|
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
|
|
|
3297
3300
|
}
|
|
3298
3301
|
return output.join("");
|
|
3299
3302
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3303
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3304
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3305
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3306
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3307
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3308
|
+
re.lastIndex = 0;
|
|
3309
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3310
|
+
}
|
|
3311
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3312
|
+
if (input.indexOf("%") === -1) {
|
|
3313
|
+
return input;
|
|
3310
3314
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3315
|
+
let output = "";
|
|
3316
|
+
for (let i = 0; i < input.length; i++) {
|
|
3317
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3318
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3319
|
+
if (isHexPair(hex3)) {
|
|
3320
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3321
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3322
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3323
|
+
output += decoded;
|
|
3324
|
+
} else {
|
|
3325
|
+
output += "%" + normalizedHex;
|
|
3326
|
+
}
|
|
3327
|
+
i += 2;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
output += input[i];
|
|
3313
3332
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3333
|
+
return output;
|
|
3334
|
+
}
|
|
3335
|
+
function normalizePathEncoding(input) {
|
|
3336
|
+
let output = "";
|
|
3337
|
+
for (let i = 0; i < input.length; i++) {
|
|
3338
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3339
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3340
|
+
if (isHexPair(hex3)) {
|
|
3341
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3342
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3343
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3344
|
+
output += decoded;
|
|
3345
|
+
} else {
|
|
3346
|
+
output += "%" + normalizedHex;
|
|
3347
|
+
}
|
|
3348
|
+
i += 2;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (isPathCharacter(input[i])) {
|
|
3353
|
+
output += input[i];
|
|
3354
|
+
} else {
|
|
3355
|
+
output += escape(input[i]);
|
|
3356
|
+
}
|
|
3316
3357
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3358
|
+
return output;
|
|
3359
|
+
}
|
|
3360
|
+
function escapePreservingEscapes(input) {
|
|
3361
|
+
let output = "";
|
|
3362
|
+
for (let i = 0; i < input.length; i++) {
|
|
3363
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3364
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3365
|
+
if (isHexPair(hex3)) {
|
|
3366
|
+
output += "%" + hex3.toUpperCase();
|
|
3367
|
+
i += 2;
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
output += escape(input[i]);
|
|
3319
3372
|
}
|
|
3320
|
-
return
|
|
3373
|
+
return output;
|
|
3321
3374
|
}
|
|
3322
3375
|
function recomposeAuthority(component) {
|
|
3323
3376
|
const uriTokens = [];
|
|
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
|
|
|
3332
3385
|
if (ipV6res.isIPV6 === true) {
|
|
3333
3386
|
host = `[${ipV6res.escapedHost}]`;
|
|
3334
3387
|
} else {
|
|
3335
|
-
host =
|
|
3388
|
+
host = reescapeHostDelimiters(host, false);
|
|
3336
3389
|
}
|
|
3337
3390
|
}
|
|
3338
3391
|
uriTokens.push(host);
|
|
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
|
|
|
3346
3399
|
module.exports = {
|
|
3347
3400
|
nonSimpleDomain,
|
|
3348
3401
|
recomposeAuthority,
|
|
3349
|
-
|
|
3402
|
+
reescapeHostDelimiters,
|
|
3403
|
+
normalizePercentEncoding,
|
|
3404
|
+
normalizePathEncoding,
|
|
3405
|
+
escapePreservingEscapes,
|
|
3350
3406
|
removeDotSegments,
|
|
3351
3407
|
isIPv4,
|
|
3352
3408
|
isUUID,
|
|
@@ -3570,12 +3626,12 @@ var require_schemes = __commonJS({
|
|
|
3570
3626
|
var require_fast_uri = __commonJS({
|
|
3571
3627
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3572
3628
|
"use strict";
|
|
3573
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3629
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3574
3630
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3575
3631
|
function normalize(uri, options) {
|
|
3576
3632
|
if (typeof uri === "string") {
|
|
3577
3633
|
uri = /** @type {T} */
|
|
3578
|
-
|
|
3634
|
+
normalizeString(uri, options);
|
|
3579
3635
|
} else if (typeof uri === "object") {
|
|
3580
3636
|
uri = /** @type {T} */
|
|
3581
3637
|
parse3(serialize(uri, options), options);
|
|
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
|
|
|
3642
3698
|
return target;
|
|
3643
3699
|
}
|
|
3644
3700
|
function equal(uriA, uriB, options) {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
} else if (typeof uriA === "object") {
|
|
3649
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3650
|
-
}
|
|
3651
|
-
if (typeof uriB === "string") {
|
|
3652
|
-
uriB = unescape(uriB);
|
|
3653
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3654
|
-
} else if (typeof uriB === "object") {
|
|
3655
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3656
|
-
}
|
|
3657
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3701
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3702
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3703
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3658
3704
|
}
|
|
3659
3705
|
function serialize(cmpts, opts) {
|
|
3660
3706
|
const component = {
|
|
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
|
|
|
3679
3725
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3680
3726
|
if (component.path !== void 0) {
|
|
3681
3727
|
if (!options.skipEscape) {
|
|
3682
|
-
component.path =
|
|
3728
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3683
3729
|
if (component.scheme !== void 0) {
|
|
3684
3730
|
component.path = component.path.split("%3A").join(":");
|
|
3685
3731
|
}
|
|
3686
3732
|
} else {
|
|
3687
|
-
component.path =
|
|
3733
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3688
3734
|
}
|
|
3689
3735
|
}
|
|
3690
3736
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
|
|
|
3719
3765
|
return uriTokens.join("");
|
|
3720
3766
|
}
|
|
3721
3767
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3722
|
-
function
|
|
3768
|
+
function getParseError(parsed, matches) {
|
|
3769
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3770
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3773
|
+
return "URI port is malformed.";
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function parseWithStatus(uri, opts) {
|
|
3723
3778
|
const options = Object.assign({}, opts);
|
|
3724
3779
|
const parsed = {
|
|
3725
3780
|
scheme: void 0,
|
|
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
|
|
|
3730
3785
|
query: void 0,
|
|
3731
3786
|
fragment: void 0
|
|
3732
3787
|
};
|
|
3788
|
+
let malformedAuthorityOrPort = false;
|
|
3733
3789
|
let isIP = false;
|
|
3734
3790
|
if (options.reference === "suffix") {
|
|
3735
3791
|
if (options.scheme) {
|
|
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3750
3806
|
if (isNaN(parsed.port)) {
|
|
3751
3807
|
parsed.port = matches[5];
|
|
3752
3808
|
}
|
|
3809
|
+
const parseError = getParseError(parsed, matches);
|
|
3810
|
+
if (parseError !== void 0) {
|
|
3811
|
+
parsed.error = parsed.error || parseError;
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3753
3814
|
if (parsed.host) {
|
|
3754
3815
|
const ipv4result = isIPv4(parsed.host);
|
|
3755
3816
|
if (ipv4result === false) {
|
|
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
|
|
|
3788
3849
|
parsed.scheme = unescape(parsed.scheme);
|
|
3789
3850
|
}
|
|
3790
3851
|
if (parsed.host !== void 0) {
|
|
3791
|
-
parsed.host = unescape(parsed.host);
|
|
3852
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3792
3853
|
}
|
|
3793
3854
|
}
|
|
3794
3855
|
if (parsed.path) {
|
|
3795
|
-
parsed.path =
|
|
3856
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3861
|
+
} catch {
|
|
3862
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3863
|
+
}
|
|
3799
3864
|
}
|
|
3800
3865
|
}
|
|
3801
3866
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3869
|
} else {
|
|
3805
3870
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3806
3871
|
}
|
|
3807
|
-
return parsed;
|
|
3872
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3873
|
+
}
|
|
3874
|
+
function parse3(uri, opts) {
|
|
3875
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3876
|
+
}
|
|
3877
|
+
function normalizeString(uri, opts) {
|
|
3878
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3879
|
+
}
|
|
3880
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3881
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3882
|
+
return {
|
|
3883
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3884
|
+
malformedAuthorityOrPort
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeComparableURI(uri, opts) {
|
|
3888
|
+
if (typeof uri === "string") {
|
|
3889
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3890
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3891
|
+
}
|
|
3892
|
+
if (typeof uri === "object") {
|
|
3893
|
+
return serialize(uri, opts);
|
|
3894
|
+
}
|
|
3808
3895
|
}
|
|
3809
3896
|
var fastUri = {
|
|
3810
3897
|
SCHEMES,
|
|
@@ -30113,6 +30200,23 @@ var StdioServerTransport = class {
|
|
|
30113
30200
|
}
|
|
30114
30201
|
};
|
|
30115
30202
|
|
|
30203
|
+
// src/audit-buffer.ts
|
|
30204
|
+
var DEFAULT_CAP = 1e3;
|
|
30205
|
+
var buffer = [];
|
|
30206
|
+
var cap = DEFAULT_CAP;
|
|
30207
|
+
function pushAuditEntry(entry) {
|
|
30208
|
+
buffer.push(entry);
|
|
30209
|
+
if (buffer.length > cap) {
|
|
30210
|
+
buffer.splice(0, buffer.length - cap);
|
|
30211
|
+
}
|
|
30212
|
+
}
|
|
30213
|
+
function readAuditEntries(limit) {
|
|
30214
|
+
const reversed = buffer.slice().reverse();
|
|
30215
|
+
if (limit === void 0 || limit >= reversed.length) return reversed;
|
|
30216
|
+
if (limit <= 0) return [];
|
|
30217
|
+
return reversed.slice(0, limit);
|
|
30218
|
+
}
|
|
30219
|
+
|
|
30116
30220
|
// src/guardrails.ts
|
|
30117
30221
|
var GuardrailError = class extends Error {
|
|
30118
30222
|
constructor(message) {
|
|
@@ -30179,15 +30283,29 @@ function checkDestructiveRateLimit(now = Date.now()) {
|
|
|
30179
30283
|
function isStoreAllowlistActive() {
|
|
30180
30284
|
return loadOptions().allowedStoreIds !== null;
|
|
30181
30285
|
}
|
|
30182
|
-
function
|
|
30183
|
-
if (
|
|
30184
|
-
|
|
30185
|
-
if (
|
|
30186
|
-
|
|
30187
|
-
|
|
30188
|
-
|
|
30189
|
-
|
|
30190
|
-
|
|
30286
|
+
function isPresent(value) {
|
|
30287
|
+
if (value === void 0 || value === null) return false;
|
|
30288
|
+
if (typeof value === "string" && value === "") return false;
|
|
30289
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
30290
|
+
return true;
|
|
30291
|
+
}
|
|
30292
|
+
function checkStoreScopedToolInput(tool, input) {
|
|
30293
|
+
const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
|
|
30294
|
+
if (toolAcceptsStoreId) {
|
|
30295
|
+
const raw = input.storeId;
|
|
30296
|
+
if (raw !== void 0 && raw !== null && raw !== "") {
|
|
30297
|
+
checkStoreAllowed(String(raw));
|
|
30298
|
+
} else if (isStoreAllowlistActive()) {
|
|
30299
|
+
throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
|
|
30300
|
+
}
|
|
30301
|
+
}
|
|
30302
|
+
if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
|
|
30303
|
+
const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
|
|
30304
|
+
if (!anyPresent) {
|
|
30305
|
+
throw new GuardrailError(
|
|
30306
|
+
`At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
|
|
30307
|
+
);
|
|
30308
|
+
}
|
|
30191
30309
|
}
|
|
30192
30310
|
}
|
|
30193
30311
|
function isDestructiveCall(tool, input) {
|
|
@@ -30252,10 +30370,46 @@ function logEvent(entry) {
|
|
|
30252
30370
|
}
|
|
30253
30371
|
}
|
|
30254
30372
|
|
|
30373
|
+
// src/redact.ts
|
|
30374
|
+
var SECRET_KEY_RE = /^(secret|password|token|api[_-]?key|bearer|authorization|signing[_-]?secret)$/i;
|
|
30375
|
+
var REDACTED = "[REDACTED]";
|
|
30376
|
+
var CIRCULAR = "[CIRCULAR]";
|
|
30377
|
+
var MAX_DEPTH = 32;
|
|
30378
|
+
function isPlainObject3(value) {
|
|
30379
|
+
if (value === null || typeof value !== "object") return false;
|
|
30380
|
+
const proto = Object.getPrototypeOf(value);
|
|
30381
|
+
return proto === Object.prototype || proto === null;
|
|
30382
|
+
}
|
|
30383
|
+
function redactInner(value, visited, depth) {
|
|
30384
|
+
if (depth > MAX_DEPTH) return CIRCULAR;
|
|
30385
|
+
if (value === null || typeof value !== "object") return value;
|
|
30386
|
+
if (Array.isArray(value)) {
|
|
30387
|
+
if (visited.has(value)) return CIRCULAR;
|
|
30388
|
+
visited.add(value);
|
|
30389
|
+
return value.map((item) => redactInner(item, visited, depth + 1));
|
|
30390
|
+
}
|
|
30391
|
+
if (!isPlainObject3(value)) return value;
|
|
30392
|
+
if (visited.has(value)) return CIRCULAR;
|
|
30393
|
+
visited.add(value);
|
|
30394
|
+
const out = {};
|
|
30395
|
+
for (const [key, val] of Object.entries(value)) {
|
|
30396
|
+
if (SECRET_KEY_RE.test(key)) {
|
|
30397
|
+
out[key] = REDACTED;
|
|
30398
|
+
} else {
|
|
30399
|
+
out[key] = redactInner(val, visited, depth + 1);
|
|
30400
|
+
}
|
|
30401
|
+
}
|
|
30402
|
+
return out;
|
|
30403
|
+
}
|
|
30404
|
+
function redactSecrets(input) {
|
|
30405
|
+
return redactInner(input, /* @__PURE__ */ new WeakSet(), 0);
|
|
30406
|
+
}
|
|
30407
|
+
|
|
30255
30408
|
// src/retry.ts
|
|
30256
30409
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
30257
30410
|
var DEFAULT_RETRY_WAIT_MS = 1e3;
|
|
30258
30411
|
var MAX_RETRY_WAIT_MS = 3e4;
|
|
30412
|
+
var OVERALL_DEADLINE_MS = 9e4;
|
|
30259
30413
|
var DEFAULT_MAX_ATTEMPTS = 4;
|
|
30260
30414
|
var BASE_BACKOFF_MS = 250;
|
|
30261
30415
|
var JITTER_FRACTION = 0.25;
|
|
@@ -30275,6 +30429,19 @@ function isAbortTimeoutError(err) {
|
|
|
30275
30429
|
const e = err;
|
|
30276
30430
|
return e.name === "TimeoutError" || e.name === "AbortError" || e.code === "ABORT_ERR";
|
|
30277
30431
|
}
|
|
30432
|
+
function isRetryTimeoutError(err) {
|
|
30433
|
+
if (!err || typeof err !== "object") return false;
|
|
30434
|
+
const e = err;
|
|
30435
|
+
return e.name === "TimeoutError" && typeof e.elapsedMs === "number" && typeof e.attempts === "number";
|
|
30436
|
+
}
|
|
30437
|
+
function makeRetryTimeoutError(elapsedMs, attempts, cause) {
|
|
30438
|
+
const err = new Error("Request timed out");
|
|
30439
|
+
err.name = "TimeoutError";
|
|
30440
|
+
err.elapsedMs = elapsedMs;
|
|
30441
|
+
err.attempts = attempts;
|
|
30442
|
+
if (cause !== void 0) err.cause = cause;
|
|
30443
|
+
return err;
|
|
30444
|
+
}
|
|
30278
30445
|
function backoffDelay(attempt, rand = Math.random) {
|
|
30279
30446
|
const base = BASE_BACKOFF_MS * 2 ** (attempt - 1);
|
|
30280
30447
|
const jitter = rand() * base * JITTER_FRACTION;
|
|
@@ -30286,31 +30453,54 @@ async function fetchWithRetry(url2, init, opts) {
|
|
|
30286
30453
|
const sleep = opts.sleep ?? defaultSleep;
|
|
30287
30454
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
30288
30455
|
const rand = opts.rand ?? Math.random;
|
|
30456
|
+
const deadlineMs = opts.deadlineMs ?? OVERALL_DEADLINE_MS;
|
|
30457
|
+
const now = opts.now ?? Date.now;
|
|
30458
|
+
const startedAt = now();
|
|
30459
|
+
const deadlineAt = startedAt + deadlineMs;
|
|
30460
|
+
let attempts = 0;
|
|
30289
30461
|
let lastError;
|
|
30462
|
+
let lastResponse;
|
|
30290
30463
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
30464
|
+
if (attempt > 1 && now() >= deadlineAt) {
|
|
30465
|
+
if (lastResponse) return lastResponse;
|
|
30466
|
+
throw makeRetryTimeoutError(now() - startedAt, attempts, lastError);
|
|
30467
|
+
}
|
|
30468
|
+
attempts = attempt;
|
|
30291
30469
|
try {
|
|
30292
30470
|
const res = await fetchImpl(url2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
30471
|
+
lastResponse = res;
|
|
30293
30472
|
if (res.status === 429) {
|
|
30294
30473
|
if (attempt === maxAttempts) return res;
|
|
30295
30474
|
const waitMs = parseRetryAfterMs(res.headers.get("retry-after"));
|
|
30296
30475
|
if (waitMs > MAX_RETRY_WAIT_MS) return res;
|
|
30476
|
+
if (now() + waitMs >= deadlineAt) return res;
|
|
30297
30477
|
await sleep(waitMs);
|
|
30298
30478
|
continue;
|
|
30299
30479
|
}
|
|
30300
30480
|
if (res.status >= 500 && res.status < 600 && opts.idempotent && attempt < maxAttempts) {
|
|
30301
|
-
|
|
30481
|
+
const waitMs = backoffDelay(attempt, rand);
|
|
30482
|
+
if (now() + waitMs >= deadlineAt) return res;
|
|
30483
|
+
await sleep(waitMs);
|
|
30302
30484
|
continue;
|
|
30303
30485
|
}
|
|
30304
30486
|
return res;
|
|
30305
30487
|
} catch (err) {
|
|
30306
30488
|
lastError = err;
|
|
30307
30489
|
if (isAbortTimeoutError(err)) {
|
|
30308
|
-
if (!opts.idempotent || attempt === maxAttempts)
|
|
30309
|
-
|
|
30490
|
+
if (!opts.idempotent || attempt === maxAttempts) {
|
|
30491
|
+
throw makeRetryTimeoutError(now() - startedAt, attempts, err);
|
|
30492
|
+
}
|
|
30493
|
+
const waitMs = backoffDelay(attempt, rand);
|
|
30494
|
+
if (now() + waitMs >= deadlineAt) {
|
|
30495
|
+
throw makeRetryTimeoutError(now() - startedAt, attempts, err);
|
|
30496
|
+
}
|
|
30497
|
+
await sleep(waitMs);
|
|
30310
30498
|
continue;
|
|
30311
30499
|
}
|
|
30312
30500
|
if (err instanceof TypeError && opts.idempotent && attempt < maxAttempts) {
|
|
30313
|
-
|
|
30501
|
+
const waitMs = backoffDelay(attempt, rand);
|
|
30502
|
+
if (now() + waitMs >= deadlineAt) throw err;
|
|
30503
|
+
await sleep(waitMs);
|
|
30314
30504
|
continue;
|
|
30315
30505
|
}
|
|
30316
30506
|
throw err;
|
|
@@ -30327,6 +30517,18 @@ var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
|
30327
30517
|
var COMMAND_TIMEOUT_MS = 1e4;
|
|
30328
30518
|
var COMMAND_MAX_BUFFER = 64 * 1024;
|
|
30329
30519
|
var cached2 = null;
|
|
30520
|
+
var testModeAnnounced = false;
|
|
30521
|
+
function announceTestModeOnce() {
|
|
30522
|
+
if (testModeAnnounced) return;
|
|
30523
|
+
testModeAnnounced = true;
|
|
30524
|
+
const line = `${JSON.stringify({
|
|
30525
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30526
|
+
event: "test_mode",
|
|
30527
|
+
message: "Using LEMONSQUEEZY_TEST_API_KEY (test mode)"
|
|
30528
|
+
})}
|
|
30529
|
+
`;
|
|
30530
|
+
process.stderr.write(line);
|
|
30531
|
+
}
|
|
30330
30532
|
function parseCommand(cmd) {
|
|
30331
30533
|
const parts = [];
|
|
30332
30534
|
let current = "";
|
|
@@ -30386,6 +30588,18 @@ async function loadApiKey() {
|
|
|
30386
30588
|
intoCache(fingerprint2, key);
|
|
30387
30589
|
return key;
|
|
30388
30590
|
}
|
|
30591
|
+
const testRaw = process.env.LEMONSQUEEZY_TEST_API_KEY;
|
|
30592
|
+
if (testRaw && testRaw.trim() !== "") {
|
|
30593
|
+
const fingerprint2 = `test:${testRaw}`;
|
|
30594
|
+
const hit2 = fromCache(fingerprint2);
|
|
30595
|
+
if (hit2 !== null) {
|
|
30596
|
+
announceTestModeOnce();
|
|
30597
|
+
return hit2;
|
|
30598
|
+
}
|
|
30599
|
+
announceTestModeOnce();
|
|
30600
|
+
intoCache(fingerprint2, testRaw);
|
|
30601
|
+
return testRaw;
|
|
30602
|
+
}
|
|
30389
30603
|
const raw = process.env.LEMONSQUEEZY_API_KEY;
|
|
30390
30604
|
if (!raw) {
|
|
30391
30605
|
throw new Error("LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_API_KEY_COMMAND environment variable is required.");
|
|
@@ -30433,6 +30647,14 @@ function buildQuery(params) {
|
|
|
30433
30647
|
function decorateError(error48, requestId) {
|
|
30434
30648
|
return requestId ? `${error48} (request_id: ${requestId})` : error48;
|
|
30435
30649
|
}
|
|
30650
|
+
function formatTimeoutMessage(err, fallbackElapsedMs) {
|
|
30651
|
+
if (isRetryTimeoutError(err)) {
|
|
30652
|
+
const seconds2 = Math.max(1, Math.round(err.elapsedMs / 1e3));
|
|
30653
|
+
return `Request timed out after ${seconds2}s (${err.attempts} attempts)`;
|
|
30654
|
+
}
|
|
30655
|
+
const seconds = Math.max(1, Math.round(fallbackElapsedMs / 1e3));
|
|
30656
|
+
return `Request timed out after ${seconds}s (1 attempts)`;
|
|
30657
|
+
}
|
|
30436
30658
|
async function apiRequest(method, path, body) {
|
|
30437
30659
|
const start = Date.now();
|
|
30438
30660
|
const apiKey = await loadApiKey();
|
|
@@ -30453,7 +30675,7 @@ async function apiRequest(method, path, body) {
|
|
|
30453
30675
|
} catch (err) {
|
|
30454
30676
|
const latency_ms2 = Date.now() - start;
|
|
30455
30677
|
if (isAbortTimeoutError(err)) {
|
|
30456
|
-
const error48 =
|
|
30678
|
+
const error48 = formatTimeoutMessage(err, latency_ms2);
|
|
30457
30679
|
logEvent({ event: "http_call", method, path, status: "timeout", latency_ms: latency_ms2, error: error48 });
|
|
30458
30680
|
return { ok: false, status: 0, error: error48 };
|
|
30459
30681
|
}
|
|
@@ -30540,7 +30762,7 @@ async function licenseRequest(path, body) {
|
|
|
30540
30762
|
} catch (err) {
|
|
30541
30763
|
const latency_ms2 = Date.now() - start;
|
|
30542
30764
|
if (isAbortTimeoutError(err)) {
|
|
30543
|
-
const error48 =
|
|
30765
|
+
const error48 = formatTimeoutMessage(err, latency_ms2);
|
|
30544
30766
|
logEvent({ event: "http_call", method: "POST", path, status: "timeout", latency_ms: latency_ms2, error: error48 });
|
|
30545
30767
|
return { ok: false, status: 0, error: error48 };
|
|
30546
30768
|
}
|
|
@@ -30663,7 +30885,7 @@ var affiliateTools = [
|
|
|
30663
30885
|
},
|
|
30664
30886
|
{
|
|
30665
30887
|
name: "ls_list_affiliates",
|
|
30666
|
-
description: "List all affiliates for the authenticated user's stores, optionally filtered by user email. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
30888
|
+
description: "List all affiliates for the authenticated user's stores, optionally filtered by user email. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool can still return affiliates tied to non-allowed stores -- the endpoint has no parent ID filter to scope by. Pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
30667
30889
|
annotations: {
|
|
30668
30890
|
title: "List affiliates",
|
|
30669
30891
|
readOnlyHint: true,
|
|
@@ -30733,7 +30955,7 @@ var checkoutTools = [
|
|
|
30733
30955
|
variantId: lsIdSchema.describe("The variant ID for the product being purchased"),
|
|
30734
30956
|
customPrice: external_exports3.number().int().min(0).optional().describe("Custom price in cents (overrides the variant price)"),
|
|
30735
30957
|
enabledVariants: external_exports3.array(lsIdSchema).optional().describe("Array of variant IDs to show on the checkout (for products with multiple variants)"),
|
|
30736
|
-
email: external_exports3.string().max(
|
|
30958
|
+
email: external_exports3.string().email().max(320).optional().describe("Prefill customer email"),
|
|
30737
30959
|
name: external_exports3.string().max(1e4).optional().describe("Prefill customer name"),
|
|
30738
30960
|
billingAddressCountry: external_exports3.string().max(1e4).optional().describe("Prefill billing country (ISO 3166-1 alpha-2)"),
|
|
30739
30961
|
billingAddressZip: external_exports3.string().max(1e4).optional().describe("Prefill billing ZIP/postal code"),
|
|
@@ -30940,7 +31162,7 @@ var discountRedemptionTools = [
|
|
|
30940
31162
|
},
|
|
30941
31163
|
{
|
|
30942
31164
|
name: "ls_list_discount_redemptions",
|
|
30943
|
-
description: "List all discount redemptions, optionally filtered by discount or order. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31165
|
+
description: "List all discount redemptions, optionally filtered by discount or order. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: discountId, orderId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
30944
31166
|
annotations: {
|
|
30945
31167
|
title: "List discount redemptions",
|
|
30946
31168
|
readOnlyHint: true,
|
|
@@ -30955,6 +31177,7 @@ var discountRedemptionTools = [
|
|
|
30955
31177
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
30956
31178
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
30957
31179
|
}),
|
|
31180
|
+
requiredFilters: ["discountId", "orderId"],
|
|
30958
31181
|
handler: listHandler("/discount-redemptions", { discountId: "discount_id", orderId: "order_id" })
|
|
30959
31182
|
}
|
|
30960
31183
|
];
|
|
@@ -31092,7 +31315,7 @@ var fileTools = [
|
|
|
31092
31315
|
},
|
|
31093
31316
|
{
|
|
31094
31317
|
name: "ls_list_files",
|
|
31095
|
-
description: "List all files, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31318
|
+
description: "List all files, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: variantId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31096
31319
|
annotations: {
|
|
31097
31320
|
title: "List files",
|
|
31098
31321
|
readOnlyHint: true,
|
|
@@ -31106,6 +31329,7 @@ var fileTools = [
|
|
|
31106
31329
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31107
31330
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31108
31331
|
}),
|
|
31332
|
+
requiredFilters: ["variantId"],
|
|
31109
31333
|
handler: listHandler("/files", { variantId: "variant_id" })
|
|
31110
31334
|
}
|
|
31111
31335
|
];
|
|
@@ -31130,7 +31354,7 @@ var licenseKeyInstanceTools = [
|
|
|
31130
31354
|
},
|
|
31131
31355
|
{
|
|
31132
31356
|
name: "ls_list_license_key_instances",
|
|
31133
|
-
description: "List all license key instances (activations), optionally filtered by license key. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31357
|
+
description: "List all license key instances (activations), optionally filtered by license key. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: licenseKeyId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31134
31358
|
annotations: {
|
|
31135
31359
|
title: "List license key instances",
|
|
31136
31360
|
readOnlyHint: true,
|
|
@@ -31144,6 +31368,7 @@ var licenseKeyInstanceTools = [
|
|
|
31144
31368
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31145
31369
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31146
31370
|
}),
|
|
31371
|
+
requiredFilters: ["licenseKeyId"],
|
|
31147
31372
|
handler: listHandler("/license-key-instances", { licenseKeyId: "license_key_id" })
|
|
31148
31373
|
}
|
|
31149
31374
|
];
|
|
@@ -31206,11 +31431,14 @@ var licenseKeyTools = [
|
|
|
31206
31431
|
idempotentHint: true,
|
|
31207
31432
|
openWorldHint: true
|
|
31208
31433
|
},
|
|
31209
|
-
// Disabling a license key revokes a customer's access.
|
|
31210
|
-
//
|
|
31211
|
-
//
|
|
31212
|
-
//
|
|
31213
|
-
|
|
31434
|
+
// Disabling a license key revokes a customer's access outright. Changing
|
|
31435
|
+
// the activation limit can also revoke access -- setting it to 0, or to
|
|
31436
|
+
// any value below the customer's current activation count, kicks
|
|
31437
|
+
// already-activated instances offline. We can't tell from the input alone
|
|
31438
|
+
// whether a given limit change shrinks or grows, so treat ANY
|
|
31439
|
+
// `activationLimit` change as destructive alongside `disabled: true`.
|
|
31440
|
+
// Benign edits (expiry) stay on the regular path.
|
|
31441
|
+
isDestructive: (input) => input.disabled === true || input.activationLimit !== void 0,
|
|
31214
31442
|
inputSchema: external_exports3.object({
|
|
31215
31443
|
licenseKeyId: lsIdSchema.describe("The license key ID to update"),
|
|
31216
31444
|
activationLimit: external_exports3.number().int().min(0).optional().describe("Maximum number of activations allowed (0 = unlimited)"),
|
|
@@ -31319,7 +31547,7 @@ var orderItemTools = [
|
|
|
31319
31547
|
},
|
|
31320
31548
|
{
|
|
31321
31549
|
name: "ls_list_order_items",
|
|
31322
|
-
description: "List all order items, optionally filtered by order or product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31550
|
+
description: "List all order items, optionally filtered by order or product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: orderId, productId, variantId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31323
31551
|
annotations: {
|
|
31324
31552
|
title: "List order items",
|
|
31325
31553
|
readOnlyHint: true,
|
|
@@ -31335,6 +31563,7 @@ var orderItemTools = [
|
|
|
31335
31563
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31336
31564
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31337
31565
|
}),
|
|
31566
|
+
requiredFilters: ["orderId", "productId", "variantId"],
|
|
31338
31567
|
handler: listHandler("/order-items", { orderId: "order_id", productId: "product_id", variantId: "variant_id" })
|
|
31339
31568
|
}
|
|
31340
31569
|
];
|
|
@@ -31458,7 +31687,7 @@ var priceTools = [
|
|
|
31458
31687
|
},
|
|
31459
31688
|
{
|
|
31460
31689
|
name: "ls_list_prices",
|
|
31461
|
-
description: "List all prices, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31690
|
+
description: "List all prices, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: variantId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31462
31691
|
annotations: {
|
|
31463
31692
|
title: "List prices",
|
|
31464
31693
|
readOnlyHint: true,
|
|
@@ -31472,6 +31701,7 @@ var priceTools = [
|
|
|
31472
31701
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31473
31702
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31474
31703
|
}),
|
|
31704
|
+
requiredFilters: ["variantId"],
|
|
31475
31705
|
handler: listHandler("/prices", { variantId: "variant_id" })
|
|
31476
31706
|
}
|
|
31477
31707
|
];
|
|
@@ -31683,7 +31913,7 @@ var subscriptionItemTools = [
|
|
|
31683
31913
|
},
|
|
31684
31914
|
{
|
|
31685
31915
|
name: "ls_list_subscription_items",
|
|
31686
|
-
description: "List all subscription items, optionally filtered by subscription or price. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
31916
|
+
description: "List all subscription items, optionally filtered by subscription or price. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: subscriptionId, priceId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31687
31917
|
annotations: {
|
|
31688
31918
|
title: "List subscription items",
|
|
31689
31919
|
readOnlyHint: true,
|
|
@@ -31698,6 +31928,7 @@ var subscriptionItemTools = [
|
|
|
31698
31928
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31699
31929
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31700
31930
|
}),
|
|
31931
|
+
requiredFilters: ["subscriptionId", "priceId"],
|
|
31701
31932
|
handler: listHandler("/subscription-items", { subscriptionId: "subscription_id", priceId: "price_id" })
|
|
31702
31933
|
},
|
|
31703
31934
|
{
|
|
@@ -31887,7 +32118,7 @@ var usageRecordTools = [
|
|
|
31887
32118
|
},
|
|
31888
32119
|
{
|
|
31889
32120
|
name: "ls_list_usage_records",
|
|
31890
|
-
description: "List all usage records, optionally filtered by subscription item. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
32121
|
+
description: "List all usage records, optionally filtered by subscription item. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: subscriptionItemId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31891
32122
|
annotations: {
|
|
31892
32123
|
title: "List usage records",
|
|
31893
32124
|
readOnlyHint: true,
|
|
@@ -31901,6 +32132,7 @@ var usageRecordTools = [
|
|
|
31901
32132
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31902
32133
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31903
32134
|
}),
|
|
32135
|
+
requiredFilters: ["subscriptionItemId"],
|
|
31904
32136
|
handler: listHandler("/usage-records", { subscriptionItemId: "subscription_item_id" })
|
|
31905
32137
|
},
|
|
31906
32138
|
{
|
|
@@ -31980,7 +32212,7 @@ var variantTools = [
|
|
|
31980
32212
|
},
|
|
31981
32213
|
{
|
|
31982
32214
|
name: "ls_list_variants",
|
|
31983
|
-
description: "List all variants, optionally filtered by product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
|
|
32215
|
+
description: "List all variants, optionally filtered by product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: productId. Even with that set, pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.",
|
|
31984
32216
|
annotations: {
|
|
31985
32217
|
title: "List variants",
|
|
31986
32218
|
readOnlyHint: true,
|
|
@@ -31994,6 +32226,7 @@ var variantTools = [
|
|
|
31994
32226
|
pageNumber: external_exports3.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
31995
32227
|
pageSize: external_exports3.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
31996
32228
|
}),
|
|
32229
|
+
requiredFilters: ["productId"],
|
|
31997
32230
|
handler: listHandler("/variants", { productId: "product_id" })
|
|
31998
32231
|
}
|
|
31999
32232
|
];
|
|
@@ -32118,7 +32351,7 @@ var webhookTools = [
|
|
|
32118
32351
|
];
|
|
32119
32352
|
|
|
32120
32353
|
// src/index.ts
|
|
32121
|
-
var version2 = true ? "0.
|
|
32354
|
+
var version2 = true ? "0.8.1" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
32122
32355
|
var subcommand = process.argv[2];
|
|
32123
32356
|
if (subcommand === "version" || subcommand === "--version") {
|
|
32124
32357
|
console.log(version2);
|
|
@@ -32152,7 +32385,6 @@ var server = new McpServer({
|
|
|
32152
32385
|
version: version2
|
|
32153
32386
|
});
|
|
32154
32387
|
for (const tool of allTools) {
|
|
32155
|
-
const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
|
|
32156
32388
|
server.tool(
|
|
32157
32389
|
tool.name,
|
|
32158
32390
|
tool.description,
|
|
@@ -32163,11 +32395,11 @@ for (const tool of allTools) {
|
|
|
32163
32395
|
const start = Date.now();
|
|
32164
32396
|
try {
|
|
32165
32397
|
if (isDestructive) checkDestructiveRateLimit();
|
|
32166
|
-
checkStoreScopedToolInput(
|
|
32398
|
+
checkStoreScopedToolInput(tool, input);
|
|
32167
32399
|
const result = await tool.handler(input);
|
|
32168
32400
|
const response = result;
|
|
32169
32401
|
const latency_ms = Date.now() - start;
|
|
32170
|
-
|
|
32402
|
+
const successEntry = {
|
|
32171
32403
|
event: "tool_call",
|
|
32172
32404
|
tool: tool.name,
|
|
32173
32405
|
status: response.ok ? "ok" : "error",
|
|
@@ -32175,8 +32407,12 @@ for (const tool of allTools) {
|
|
|
32175
32407
|
request_id: response.requestId,
|
|
32176
32408
|
error: response.ok ? void 0 : response.error,
|
|
32177
32409
|
audit: isDestructive ? true : void 0,
|
|
32178
|
-
inputs: isDestructive ? input : void 0
|
|
32179
|
-
}
|
|
32410
|
+
inputs: isDestructive ? redactSecrets(input) : void 0
|
|
32411
|
+
};
|
|
32412
|
+
logEvent(successEntry);
|
|
32413
|
+
if (isDestructive) {
|
|
32414
|
+
pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
|
|
32415
|
+
}
|
|
32180
32416
|
if (!response.ok) {
|
|
32181
32417
|
return {
|
|
32182
32418
|
content: [
|
|
@@ -32195,15 +32431,19 @@ for (const tool of allTools) {
|
|
|
32195
32431
|
} catch (err) {
|
|
32196
32432
|
const message = err instanceof Error ? err.message : String(err);
|
|
32197
32433
|
const latency_ms = Date.now() - start;
|
|
32198
|
-
|
|
32434
|
+
const errorEntry = {
|
|
32199
32435
|
event: "tool_call",
|
|
32200
32436
|
tool: tool.name,
|
|
32201
32437
|
status: err instanceof GuardrailError ? "guardrail_block" : "exception",
|
|
32202
32438
|
latency_ms,
|
|
32203
32439
|
error: message,
|
|
32204
32440
|
audit: isDestructive ? true : void 0,
|
|
32205
|
-
inputs: isDestructive ? input : void 0
|
|
32206
|
-
}
|
|
32441
|
+
inputs: isDestructive ? redactSecrets(input) : void 0
|
|
32442
|
+
};
|
|
32443
|
+
logEvent(errorEntry);
|
|
32444
|
+
if (isDestructive) {
|
|
32445
|
+
pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
|
|
32446
|
+
}
|
|
32207
32447
|
return {
|
|
32208
32448
|
content: [{ type: "text", text: `Error: ${message}` }],
|
|
32209
32449
|
isError: true
|
|
@@ -32212,6 +32452,27 @@ for (const tool of allTools) {
|
|
|
32212
32452
|
}
|
|
32213
32453
|
);
|
|
32214
32454
|
}
|
|
32455
|
+
server.resource(
|
|
32456
|
+
"Recent destructive-call audit log",
|
|
32457
|
+
"lemonsqueezy://audit-log",
|
|
32458
|
+
{
|
|
32459
|
+
description: "The most recent destructive tool calls and their outcomes (rate limit, refund cap, etc.). Bounded ring buffer; resets on server restart.",
|
|
32460
|
+
mimeType: "application/x-ndjson"
|
|
32461
|
+
},
|
|
32462
|
+
async (uri) => {
|
|
32463
|
+
const entries = readAuditEntries();
|
|
32464
|
+
const text = entries.map((e) => JSON.stringify(e)).join("\n");
|
|
32465
|
+
return {
|
|
32466
|
+
contents: [
|
|
32467
|
+
{
|
|
32468
|
+
uri: uri.href,
|
|
32469
|
+
mimeType: "application/x-ndjson",
|
|
32470
|
+
text
|
|
32471
|
+
}
|
|
32472
|
+
]
|
|
32473
|
+
};
|
|
32474
|
+
}
|
|
32475
|
+
);
|
|
32215
32476
|
var transport = new StdioServerTransport();
|
|
32216
32477
|
await server.connect(transport);
|
|
32217
32478
|
//# sourceMappingURL=index.js.map
|