@yawlabs/lemonsqueezy-mcp 0.7.1 → 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 +217 -60
  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) {
@@ -30413,6 +30517,18 @@ var CACHE_TTL_MS = 60 * 60 * 1e3;
30413
30517
  var COMMAND_TIMEOUT_MS = 1e4;
30414
30518
  var COMMAND_MAX_BUFFER = 64 * 1024;
30415
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
+ }
30416
30532
  function parseCommand(cmd) {
30417
30533
  const parts = [];
30418
30534
  let current = "";
@@ -30472,6 +30588,18 @@ async function loadApiKey() {
30472
30588
  intoCache(fingerprint2, key);
30473
30589
  return key;
30474
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
+ }
30475
30603
  const raw = process.env.LEMONSQUEEZY_API_KEY;
30476
30604
  if (!raw) {
30477
30605
  throw new Error("LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_API_KEY_COMMAND environment variable is required.");
@@ -32223,7 +32351,7 @@ var webhookTools = [
32223
32351
  ];
32224
32352
 
32225
32353
  // src/index.ts
32226
- var version2 = true ? "0.7.1" : (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;
32227
32355
  var subcommand = process.argv[2];
32228
32356
  if (subcommand === "version" || subcommand === "--version") {
32229
32357
  console.log(version2);
@@ -32271,7 +32399,7 @@ for (const tool of allTools) {
32271
32399
  const result = await tool.handler(input);
32272
32400
  const response = result;
32273
32401
  const latency_ms = Date.now() - start;
32274
- logEvent({
32402
+ const successEntry = {
32275
32403
  event: "tool_call",
32276
32404
  tool: tool.name,
32277
32405
  status: response.ok ? "ok" : "error",
@@ -32280,7 +32408,11 @@ for (const tool of allTools) {
32280
32408
  error: response.ok ? void 0 : response.error,
32281
32409
  audit: isDestructive ? true : void 0,
32282
32410
  inputs: isDestructive ? redactSecrets(input) : void 0
32283
- });
32411
+ };
32412
+ logEvent(successEntry);
32413
+ if (isDestructive) {
32414
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32415
+ }
32284
32416
  if (!response.ok) {
32285
32417
  return {
32286
32418
  content: [
@@ -32299,7 +32431,7 @@ for (const tool of allTools) {
32299
32431
  } catch (err) {
32300
32432
  const message = err instanceof Error ? err.message : String(err);
32301
32433
  const latency_ms = Date.now() - start;
32302
- logEvent({
32434
+ const errorEntry = {
32303
32435
  event: "tool_call",
32304
32436
  tool: tool.name,
32305
32437
  status: err instanceof GuardrailError ? "guardrail_block" : "exception",
@@ -32307,7 +32439,11 @@ for (const tool of allTools) {
32307
32439
  error: message,
32308
32440
  audit: isDestructive ? true : void 0,
32309
32441
  inputs: isDestructive ? redactSecrets(input) : void 0
32310
- });
32442
+ };
32443
+ logEvent(errorEntry);
32444
+ if (isDestructive) {
32445
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32446
+ }
32311
32447
  return {
32312
32448
  content: [{ type: "text", text: `Error: ${message}` }],
32313
32449
  isError: true
@@ -32316,6 +32452,27 @@ for (const tool of allTools) {
32316
32452
  }
32317
32453
  );
32318
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
+ );
32319
32476
  var transport = new StdioServerTransport();
32320
32477
  await server.connect(transport);
32321
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.1",
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>",