@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.
Files changed (3) hide show
  1. package/README.md +76 -4
  2. package/dist/index.js +355 -94
  3. 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
- Releases are cut locally there is no CI pipeline. From a clean checkout of `main`:
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
- ./release.sh 0.6.0
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 script lints, tests, builds, bumps the version, commits and tags, pushes to `origin`, publishes to npm, and creates a GitHub release. Each step is idempotent re-running with the same version after a partial failure resumes from where it stopped.
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
- One-time setup on the release machine:
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(buffer) {
3134
- buffer.length = 0;
3136
+ function consumeIsZone(buffer2) {
3137
+ buffer2.length = 0;
3135
3138
  return true;
3136
3139
  }
3137
- function consumeHextets(buffer, address, output) {
3138
- if (buffer.length) {
3139
- const hex3 = stringArrayToHexStripped(buffer);
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
- buffer.length = 0;
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 buffer = [];
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(buffer, address, output)) {
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(buffer, address, output)) {
3183
+ if (!consume(buffer2, address, output)) {
3181
3184
  break;
3182
3185
  }
3183
3186
  consume = consumeIsZone;
3184
3187
  } else {
3185
- buffer.push(cursor);
3188
+ buffer2.push(cursor);
3186
3189
  continue;
3187
3190
  }
3188
3191
  }
3189
- if (buffer.length) {
3192
+ if (buffer2.length) {
3190
3193
  if (consume === consumeIsZone) {
3191
- output.zone = buffer.join("");
3194
+ output.zone = buffer2.join("");
3192
3195
  } else if (endIpv6) {
3193
- address.push(buffer.join(""));
3196
+ address.push(buffer2.join(""));
3194
3197
  } else {
3195
- address.push(stringArrayToHexStripped(buffer));
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
- function normalizeComponentEncoding(component, esc2) {
3301
- const func = esc2 !== true ? escape : unescape;
3302
- if (component.scheme !== void 0) {
3303
- component.scheme = func(component.scheme);
3304
- }
3305
- if (component.userinfo !== void 0) {
3306
- component.userinfo = func(component.userinfo);
3307
- }
3308
- if (component.host !== void 0) {
3309
- component.host = func(component.host);
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
- if (component.path !== void 0) {
3312
- component.path = func(component.path);
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
- if (component.query !== void 0) {
3315
- component.query = func(component.query);
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
- if (component.fragment !== void 0) {
3318
- component.fragment = func(component.fragment);
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 component;
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 = component.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
- normalizeComponentEncoding,
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, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
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
- serialize(parse3(uri, options), options);
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
- if (typeof uriA === "string") {
3646
- uriA = unescape(uriA);
3647
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
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 = escape(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 = unescape(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 parse3(uri, opts) {
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 = escape(unescape(parsed.path));
3856
+ parsed.path = normalizePathEncoding(parsed.path);
3796
3857
  }
3797
3858
  if (parsed.fragment) {
3798
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
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 checkStoreScopedToolInput(toolAcceptsStoreId, input) {
30183
- if (!toolAcceptsStoreId) return;
30184
- const raw = input.storeId;
30185
- if (raw !== void 0 && raw !== null && raw !== "") {
30186
- checkStoreAllowed(String(raw));
30187
- return;
30188
- }
30189
- if (isStoreAllowlistActive()) {
30190
- throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
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
- await sleep(backoffDelay(attempt, rand));
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) throw err;
30309
- await sleep(backoffDelay(attempt, rand));
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
- await sleep(backoffDelay(attempt, rand));
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 = `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`;
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 = `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`;
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(1e4).optional().describe("Prefill customer email"),
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. Tag the call as
31210
- // destructive only when `disabled: true` so the rate limiter and audit log
31211
- // engage on revocation, while benign edits (expiry, activation limit) stay
31212
- // on the regular path.
31213
- isDestructive: (input) => input.disabled === true,
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.7.0" : (await null).createRequire(import.meta.url)("../package.json").version;
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(toolAcceptsStoreId, input);
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
- logEvent({
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
- logEvent({
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "LemonSqueezy MCP server for managing your store from AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",