@yawlabs/lemonsqueezy-mcp 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +104 -7
  2. package/dist/index.js +365 -61
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -2,12 +2,47 @@
2
2
 
3
3
  MCP server for the [LemonSqueezy](https://lemonsqueezy.com) API. Manage your store, products, customers, subscriptions, discounts, license keys, and more from any MCP-compatible AI assistant.
4
4
 
5
+ [![Add to mcp.hosting](https://mcp.hosting/install-button.svg)](https://mcp.hosting/install?name=LemonSqueezy&command=npx&args=-y%2C%40yawlabs%2Flemonsqueezy-mcp&description=LemonSqueezy%20store%20management%20-%20products%2C%20orders%2C%20subscriptions%2C%20license%20keys&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Flemonsqueezy-mcp)
6
+
7
+ One click adds this to your [mcp.hosting](https://mcp.hosting) account so it syncs to every MCP client you use. Or install manually below.
8
+
5
9
  ## Quick start
6
10
 
7
11
  ```bash
8
12
  npx @yawlabs/lemonsqueezy-mcp
9
13
  ```
10
14
 
15
+ Or one-click install via Smithery:
16
+
17
+ ```bash
18
+ npx -y @smithery/cli install @yawlabs/lemonsqueezy-mcp --client claude
19
+ ```
20
+
21
+ Smithery prompts for your env vars (API key, optional guardrails) and writes the config into your client for you.
22
+
23
+ ## What it looks like
24
+
25
+ Once configured, you can ask your AI assistant store-management questions in plain English and it routes them through the MCP tools:
26
+
27
+ ```
28
+ You: How much did we make from the "Pro Annual" plan last month?
29
+ Claude: [calls ls_list_subscriptions, ls_get_variant, ls_list_subscription_invoices]
30
+ Pro Annual brought in $14,280 across 84 active subscriptions in April.
31
+ Three of those were upgrades from monthly; none churned.
32
+
33
+ You: Refund order #LS-1234 in full.
34
+ Claude: [calls ls_get_order to fetch the total, then ls_refund_order with amount = total]
35
+ Refunded $99.00 against order LS-1234. The customer's card will see the
36
+ credit in 5-10 business days.
37
+
38
+ You: Disable license key abc-123 for the customer who reported abuse.
39
+ Claude: [calls ls_list_license_keys to find the ID, then ls_update_license_key with disabled: true]
40
+ License key disabled. Their existing activations will fail validation
41
+ on the next check.
42
+ ```
43
+
44
+ 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.
45
+
11
46
  ## Setup
12
47
 
13
48
  Set your LemonSqueezy API key as an environment variable:
@@ -18,6 +53,17 @@ export LEMONSQUEEZY_API_KEY="your-api-key"
18
53
 
19
54
  Get your API key from your [LemonSqueezy dashboard](https://app.lemonsqueezy.com/settings/api).
20
55
 
56
+ ### Docker
57
+
58
+ 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).
59
+
60
+ ```bash
61
+ docker build -t yawlabs/lemonsqueezy-mcp .
62
+ docker run --rm -i -e LEMONSQUEEZY_API_KEY="your-api-key" yawlabs/lemonsqueezy-mcp
63
+ ```
64
+
65
+ A byte-identical `Containerfile` is also provided for Podman users.
66
+
21
67
  ### Claude Code
22
68
 
23
69
  Add to your MCP config:
@@ -174,9 +220,12 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
174
220
  | --- | --- |
175
221
  | `LEMONSQUEEZY_API_KEY` | LemonSqueezy API token. |
176
222
  | `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. |
223
+ | `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
224
  | `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
225
  | `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. |
179
226
  | `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. |
227
+ | `LEMONSQUEEZY_DISABLE_CLASSES` | Comma-separated list of [authority classes](#authority-classes) to refuse outright. Any tool whose class is listed returns a `guardrail_block` before the API call is attempted. Example: `LEMONSQUEEZY_DISABLE_CLASSES=money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup. |
228
+ | `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` | Per-class rolling rate limits, comma-separated. Each entry is `class:N`, `class:N/m`, or `class:N/h` (bare numbers default to per-minute). Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` — both must pass. In-process per server instance. |
180
229
  | `LEMONSQUEEZY_LOG` | Structured-log verbosity to stderr. Set to `all` (or legacy `json`) to log every tool and HTTP call, `audit` to log only destructive-call audit entries plus errors (recommended for production), `error` to log only failures. Unset: no logs. Destructive calls are tagged `audit: true` and include their inputs. |
181
230
 
182
231
  ### Logging format
@@ -187,15 +236,42 @@ Each line: `{ts, event, tool?, method?, path?, status, latency_ms, request_id?,
187
236
 
188
237
  HTTP errors include the upstream `X-Request-Id` when present, so support tickets to LemonSqueezy can reference the exact call.
189
238
 
239
+ ## Authority classes
240
+
241
+ Every tool is tagged with an **authority class** — a label for the kind of business authority a caller needs to invoke it. The class is separate from the binary destructive/read-only annotation: a customer-record read and a product list are both reads, but only one returns PII; a checkout creation and a refund are both writes, but only one moves money. Granular classes let an operator gate by the dimension that matters, instead of being stuck with a single "destructive" toggle.
242
+
243
+ | Class | What it covers | Example tools |
244
+ | --- | --- | --- |
245
+ | `read` | Safe reads (list/get) that don't return customer PII as the primary payload. | `ls_list_orders`, `ls_get_product`, `ls_validate_license` |
246
+ | `pii` | Reads or writes whose primary payload is a customer record. | `ls_list_customers`, `ls_create_customer`, `ls_archive_customer` |
247
+ | `mutate` | Safe mutations: checkouts, discounts, invoice generation, usage records. | `ls_create_checkout`, `ls_create_discount`, `ls_generate_order_invoice` |
248
+ | `money` | Money movement. Irreversible at the payment layer. | `ls_refund_order`, `ls_refund_subscription_invoice` |
249
+ | `recurring` | Subscription state changes that affect recurring revenue. | `ls_update_subscription`, `ls_cancel_subscription`, `ls_update_subscription_item` |
250
+ | `key` | License-key admin (activate, deactivate, disable, change activation limit). | `ls_update_license_key`, `ls_activate_license`, `ls_deactivate_license` |
251
+ | `webhook` | Webhook configuration — affects the trust surface other systems rely on. | `ls_create_webhook`, `ls_update_webhook`, `ls_delete_webhook` |
252
+
253
+ Note: `ls_get_order` returns customer fields incidentally, but its primary payload is the order — it stays in `read`, not `pii`. The class is reserved for tools whose *primary purpose* is the customer record. If you need to deny all access to customer-shaped data, prefer a scoped API key over class-disable alone.
254
+
255
+ `LEMONSQUEEZY_DISABLE_CLASSES` blocks a class outright. `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` caps the call rate per class. Both are opt-in; with neither set, behavior is unchanged from prior versions.
256
+
257
+ ## Resources
258
+
259
+ The server exposes one MCP Resource for clients that prefer structural retrieval over parsing stderr:
260
+
261
+ | URI | MIME type | Contents |
262
+ | --- | --- | --- |
263
+ | `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. |
264
+
190
265
  ## Operating the server unattended
191
266
 
192
267
  For unattended/agentic use against a live store, we recommend:
193
268
 
194
269
  1. Use a LemonSqueezy API key scoped to the specific store(s) the agent may touch — this is the only authoritative store boundary for tools that route by their own resource ID (refunds, cancels, archive, delete-webhook, etc.). Set `LEMONSQUEEZY_ALLOWED_STORE_IDS` to the same set as a defense-in-depth gate on the tools that *do* take a `storeId`.
195
270
  2. Set `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` to a per-call cap well below any single-refund expectation.
196
- 3. Set `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` to a small number (e.g. 5/min) as a runaway-agent circuit breaker.
197
- 4. Set `LEMONSQUEEZY_LOG=audit` and ship stderr to your log aggregator. The `audit` level keeps every destructive-call entry plus errors but drops successful reads so log volume stays bounded over weeks of operation. Alert on `status: "guardrail_block"` or elevated error rates per tool. Use `LEMONSQUEEZY_LOG=all` while debugging.
198
- 5. Run `LEMONSQUEEZY_API_KEY_COMMAND` against a vault-backed secret so credentials can rotate without restarting the server process. The API client invalidates its in-process key cache automatically on a 401/403, so a rotated upstream key picks up on the next request rather than waiting on the 1h TTL.
271
+ 3. Set `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` to a small number (e.g. 5/min) as a runaway-agent circuit breaker. For finer control, add `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS=money:2/h,recurring:5/h,key:10/m` so each [authority class](#authority-classes) has its own ceiling.
272
+ 4. If a class shouldn't be reachable at all (e.g. an analytics agent that needs only `read`), set `LEMONSQUEEZY_DISABLE_CLASSES` to refuse the rest outright calls return a `guardrail_block` before the API is touched.
273
+ 5. Set `LEMONSQUEEZY_LOG=audit` and ship stderr to your log aggregator. The `audit` level keeps every destructive-call entry plus errors but drops successful reads so log volume stays bounded over weeks of operation. Alert on `status: "guardrail_block"` or elevated error rates per tool. Use `LEMONSQUEEZY_LOG=all` while debugging.
274
+ 6. Run `LEMONSQUEEZY_API_KEY_COMMAND` against a vault-backed secret so credentials can rotate without restarting the server process. The API client invalidates its in-process key cache automatically on a 401/403, so a rotated upstream key picks up on the next request rather than waiting on the 1h TTL.
199
275
 
200
276
  What the server does **not** do and you must own at the caller level:
201
277
 
@@ -216,15 +292,36 @@ npm run test:integration # requires LEMONSQUEEZY_TEST_API_KEY + LEMONSQUEEZY_TE
216
292
 
217
293
  ## Releasing
218
294
 
219
- Releases are cut locally there is no CI pipeline. From a clean checkout of `main`:
295
+ Two paths from a clean checkout of `main`. Both produce the same artifact (npm publish with provenance + GitHub release).
296
+
297
+ ### 1. Tag-and-let-CI (preferred)
220
298
 
221
299
  ```bash
222
- ./release.sh 0.6.0
300
+ # 1. Bump version
301
+ npm version X.Y.Z --no-git-tag-version
302
+
303
+ # 2. Commit
304
+ git add package.json && git commit -m "vX.Y.Z"
305
+
306
+ # 3. Annotated tag (lightweight tags are silently skipped by --follow-tags)
307
+ git tag -a vX.Y.Z -m "vX.Y.Z"
308
+
309
+ # 4. Push commit + tag
310
+ git push origin main --follow-tags
311
+
312
+ # 5. Confirm the Release workflow fired (not just CI on the bump commit)
313
+ gh run list --limit 2
223
314
  ```
224
315
 
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.
316
+ 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.
317
+
318
+ ### 2. Local end-to-end
319
+
320
+ ```bash
321
+ ./release.sh X.Y.Z
322
+ ```
226
323
 
227
- One-time setup on the release machine:
324
+ 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
325
 
229
326
  ```bash
230
327
  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) {
@@ -30120,8 +30224,13 @@ var GuardrailError = class extends Error {
30120
30224
  this.name = "GuardrailError";
30121
30225
  }
30122
30226
  };
30227
+ var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
30228
+ function isAuthorityClass(s) {
30229
+ return AUTHORITY_CLASSES.includes(s);
30230
+ }
30123
30231
  var cachedOptions = null;
30124
30232
  var destructiveTimestamps = [];
30233
+ var classTimestamps = /* @__PURE__ */ new Map();
30125
30234
  function readNumber(name, raw) {
30126
30235
  if (raw === void 0 || raw.trim() === "") return null;
30127
30236
  const n = Number(raw);
@@ -30130,6 +30239,59 @@ function readNumber(name, raw) {
30130
30239
  }
30131
30240
  return n;
30132
30241
  }
30242
+ function parseDisabledClasses(raw) {
30243
+ if (!raw || raw.trim() === "") return null;
30244
+ const out = /* @__PURE__ */ new Set();
30245
+ for (const part of raw.split(",")) {
30246
+ const cls = part.trim();
30247
+ if (!cls) continue;
30248
+ if (!isAuthorityClass(cls)) {
30249
+ throw new Error(
30250
+ `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30251
+ );
30252
+ }
30253
+ out.add(cls);
30254
+ }
30255
+ return out.size > 0 ? out : null;
30256
+ }
30257
+ function parseClassRateLimits(raw) {
30258
+ if (!raw || raw.trim() === "") return null;
30259
+ const out = /* @__PURE__ */ new Map();
30260
+ for (const part of raw.split(",")) {
30261
+ const segment = part.trim();
30262
+ if (!segment) continue;
30263
+ const colon = segment.indexOf(":");
30264
+ if (colon < 0) {
30265
+ throw new Error(
30266
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
30267
+ );
30268
+ }
30269
+ const cls = segment.slice(0, colon).trim();
30270
+ if (!isAuthorityClass(cls)) {
30271
+ throw new Error(
30272
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30273
+ );
30274
+ }
30275
+ const specRaw = segment.slice(colon + 1).trim();
30276
+ const slash = specRaw.indexOf("/");
30277
+ const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
30278
+ const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
30279
+ const n = Number(numPart);
30280
+ if (!Number.isFinite(n) || n < 0 || numPart === "") {
30281
+ throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
30282
+ }
30283
+ let windowMs;
30284
+ if (unitPart === "m") windowMs = 6e4;
30285
+ else if (unitPart === "h") windowMs = 36e5;
30286
+ else {
30287
+ throw new Error(
30288
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
30289
+ );
30290
+ }
30291
+ out.set(cls, { limit: n, windowMs });
30292
+ }
30293
+ return out.size > 0 ? out : null;
30294
+ }
30133
30295
  function loadOptions() {
30134
30296
  if (cachedOptions) return cachedOptions;
30135
30297
  const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
@@ -30145,7 +30307,9 @@ function loadOptions() {
30145
30307
  rateLimitPerMinute: readNumber(
30146
30308
  "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
30147
30309
  process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
30148
- )
30310
+ ),
30311
+ disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
30312
+ classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
30149
30313
  };
30150
30314
  return cachedOptions;
30151
30315
  }
@@ -30208,6 +30372,30 @@ function isDestructiveCall(tool, input) {
30208
30372
  if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
30209
30373
  return tool.annotations?.destructiveHint === true;
30210
30374
  }
30375
+ function checkClassAllowed(cls) {
30376
+ const o = loadOptions();
30377
+ if (!o.disabledClasses) return;
30378
+ if (o.disabledClasses.has(cls)) {
30379
+ throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
30380
+ }
30381
+ }
30382
+ function checkClassRateLimit(cls, now = Date.now()) {
30383
+ const o = loadOptions();
30384
+ if (!o.classRateLimits) return;
30385
+ const spec = o.classRateLimits.get(cls);
30386
+ if (!spec) return;
30387
+ const cutoff = now - spec.windowMs;
30388
+ const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
30389
+ if (list.length >= spec.limit) {
30390
+ const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
30391
+ classTimestamps.set(cls, list);
30392
+ throw new GuardrailError(
30393
+ `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
30394
+ );
30395
+ }
30396
+ list.push(now);
30397
+ classTimestamps.set(cls, list);
30398
+ }
30211
30399
 
30212
30400
  // src/logger.ts
30213
30401
  function getLogLevel() {
@@ -30413,6 +30601,18 @@ var CACHE_TTL_MS = 60 * 60 * 1e3;
30413
30601
  var COMMAND_TIMEOUT_MS = 1e4;
30414
30602
  var COMMAND_MAX_BUFFER = 64 * 1024;
30415
30603
  var cached2 = null;
30604
+ var testModeAnnounced = false;
30605
+ function announceTestModeOnce() {
30606
+ if (testModeAnnounced) return;
30607
+ testModeAnnounced = true;
30608
+ const line = `${JSON.stringify({
30609
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
30610
+ event: "test_mode",
30611
+ message: "Using LEMONSQUEEZY_TEST_API_KEY (test mode)"
30612
+ })}
30613
+ `;
30614
+ process.stderr.write(line);
30615
+ }
30416
30616
  function parseCommand(cmd) {
30417
30617
  const parts = [];
30418
30618
  let current = "";
@@ -30472,6 +30672,18 @@ async function loadApiKey() {
30472
30672
  intoCache(fingerprint2, key);
30473
30673
  return key;
30474
30674
  }
30675
+ const testRaw = process.env.LEMONSQUEEZY_TEST_API_KEY;
30676
+ if (testRaw && testRaw.trim() !== "") {
30677
+ const fingerprint2 = `test:${testRaw}`;
30678
+ const hit2 = fromCache(fingerprint2);
30679
+ if (hit2 !== null) {
30680
+ announceTestModeOnce();
30681
+ return hit2;
30682
+ }
30683
+ announceTestModeOnce();
30684
+ intoCache(fingerprint2, testRaw);
30685
+ return testRaw;
30686
+ }
30475
30687
  const raw = process.env.LEMONSQUEEZY_API_KEY;
30476
30688
  if (!raw) {
30477
30689
  throw new Error("LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_API_KEY_COMMAND environment variable is required.");
@@ -30741,6 +30953,7 @@ async function apiDelete(path) {
30741
30953
  var affiliateTools = [
30742
30954
  {
30743
30955
  name: "ls_get_affiliate",
30956
+ authorityClass: "read",
30744
30957
  description: "Get a specific affiliate by ID, including commission rate, status, and earnings.",
30745
30958
  annotations: {
30746
30959
  title: "Get affiliate",
@@ -30757,6 +30970,7 @@ var affiliateTools = [
30757
30970
  },
30758
30971
  {
30759
30972
  name: "ls_list_affiliates",
30973
+ authorityClass: "read",
30760
30974
  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.",
30761
30975
  annotations: {
30762
30976
  title: "List affiliates",
@@ -30779,6 +30993,7 @@ var affiliateTools = [
30779
30993
  var checkoutTools = [
30780
30994
  {
30781
30995
  name: "ls_get_checkout",
30996
+ authorityClass: "read",
30782
30997
  description: "Get a specific checkout by ID, including URL, expiry, and custom data.",
30783
30998
  annotations: {
30784
30999
  title: "Get checkout",
@@ -30795,6 +31010,7 @@ var checkoutTools = [
30795
31010
  },
30796
31011
  {
30797
31012
  name: "ls_list_checkouts",
31013
+ authorityClass: "read",
30798
31014
  description: "List all checkouts, optionally filtered by store or variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
30799
31015
  annotations: {
30800
31016
  title: "List checkouts",
@@ -30814,6 +31030,7 @@ var checkoutTools = [
30814
31030
  },
30815
31031
  {
30816
31032
  name: "ls_create_checkout",
31033
+ authorityClass: "mutate",
30817
31034
  description: "Create a new checkout URL for a product variant. Returns a URL where the customer can complete their purchase. Supports custom pricing, prefilled customer data, and checkout customization.",
30818
31035
  annotations: {
30819
31036
  title: "Create checkout",
@@ -30874,6 +31091,7 @@ var checkoutTools = [
30874
31091
  var customerTools = [
30875
31092
  {
30876
31093
  name: "ls_get_customer",
31094
+ authorityClass: "pii",
30877
31095
  description: "Get a specific customer by ID, including name, email, city, country, MRR, total revenue, and customer portal URL.",
30878
31096
  annotations: {
30879
31097
  title: "Get customer",
@@ -30890,6 +31108,7 @@ var customerTools = [
30890
31108
  },
30891
31109
  {
30892
31110
  name: "ls_list_customers",
31111
+ authorityClass: "pii",
30893
31112
  description: "List all customers, optionally filtered by store or email. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
30894
31113
  annotations: {
30895
31114
  title: "List customers",
@@ -30909,6 +31128,7 @@ var customerTools = [
30909
31128
  },
30910
31129
  {
30911
31130
  name: "ls_create_customer",
31131
+ authorityClass: "pii",
30912
31132
  description: "Create a new customer in a store.",
30913
31133
  annotations: {
30914
31134
  title: "Create customer",
@@ -30946,6 +31166,7 @@ var customerTools = [
30946
31166
  },
30947
31167
  {
30948
31168
  name: "ls_update_customer",
31169
+ authorityClass: "pii",
30949
31170
  description: "Update an existing customer's name, email, city, region, country, or status. The only supported status value is 'archived' \u2014 use ls_archive_customer for the dedicated, audit-tagged path.",
30950
31171
  annotations: {
30951
31172
  title: "Update customer",
@@ -30991,6 +31212,7 @@ var customerTools = [
30991
31212
  },
30992
31213
  {
30993
31214
  name: "ls_archive_customer",
31215
+ authorityClass: "pii",
30994
31216
  description: "Archive a customer. Sets their status to 'archived'. This is reversible by updating their status back.",
30995
31217
  annotations: {
30996
31218
  title: "Archive customer",
@@ -31018,6 +31240,7 @@ var customerTools = [
31018
31240
  var discountRedemptionTools = [
31019
31241
  {
31020
31242
  name: "ls_get_discount_redemption",
31243
+ authorityClass: "read",
31021
31244
  description: "Get a specific discount redemption by ID, showing when and where a discount was used.",
31022
31245
  annotations: {
31023
31246
  title: "Get discount redemption",
@@ -31034,6 +31257,7 @@ var discountRedemptionTools = [
31034
31257
  },
31035
31258
  {
31036
31259
  name: "ls_list_discount_redemptions",
31260
+ authorityClass: "read",
31037
31261
  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.",
31038
31262
  annotations: {
31039
31263
  title: "List discount redemptions",
@@ -31058,6 +31282,7 @@ var discountRedemptionTools = [
31058
31282
  var discountTools = [
31059
31283
  {
31060
31284
  name: "ls_get_discount",
31285
+ authorityClass: "read",
31061
31286
  description: "Get a specific discount by ID, including code, amount, type, and usage limits.",
31062
31287
  annotations: {
31063
31288
  title: "Get discount",
@@ -31074,6 +31299,7 @@ var discountTools = [
31074
31299
  },
31075
31300
  {
31076
31301
  name: "ls_list_discounts",
31302
+ authorityClass: "read",
31077
31303
  description: "List all discounts, optionally filtered by store. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31078
31304
  annotations: {
31079
31305
  title: "List discounts",
@@ -31092,6 +31318,7 @@ var discountTools = [
31092
31318
  },
31093
31319
  {
31094
31320
  name: "ls_create_discount",
31321
+ authorityClass: "mutate",
31095
31322
  description: "Create a new discount code. Supports percentage or fixed amount discounts with optional duration and usage limits.",
31096
31323
  annotations: {
31097
31324
  title: "Create discount",
@@ -31150,6 +31377,7 @@ var discountTools = [
31150
31377
  },
31151
31378
  {
31152
31379
  name: "ls_delete_discount",
31380
+ authorityClass: "mutate",
31153
31381
  description: "Permanently delete a discount. This is irreversible.",
31154
31382
  annotations: {
31155
31383
  title: "Delete discount",
@@ -31171,6 +31399,7 @@ var discountTools = [
31171
31399
  var fileTools = [
31172
31400
  {
31173
31401
  name: "ls_get_file",
31402
+ authorityClass: "read",
31174
31403
  description: "Get a specific file by ID, including name, size, download URL, and associated variant.",
31175
31404
  annotations: {
31176
31405
  title: "Get file",
@@ -31187,6 +31416,7 @@ var fileTools = [
31187
31416
  },
31188
31417
  {
31189
31418
  name: "ls_list_files",
31419
+ authorityClass: "read",
31190
31420
  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.",
31191
31421
  annotations: {
31192
31422
  title: "List files",
@@ -31210,6 +31440,7 @@ var fileTools = [
31210
31440
  var licenseKeyInstanceTools = [
31211
31441
  {
31212
31442
  name: "ls_get_license_key_instance",
31443
+ authorityClass: "read",
31213
31444
  description: "Get a specific license key instance (activation) by ID, including instance name and creation date.",
31214
31445
  annotations: {
31215
31446
  title: "Get license key instance",
@@ -31226,6 +31457,7 @@ var licenseKeyInstanceTools = [
31226
31457
  },
31227
31458
  {
31228
31459
  name: "ls_list_license_key_instances",
31460
+ authorityClass: "read",
31229
31461
  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.",
31230
31462
  annotations: {
31231
31463
  title: "List license key instances",
@@ -31249,6 +31481,7 @@ var licenseKeyInstanceTools = [
31249
31481
  var licenseKeyTools = [
31250
31482
  {
31251
31483
  name: "ls_get_license_key",
31484
+ authorityClass: "read",
31252
31485
  description: "Get a specific license key by ID, including key value, status, activation limit, and expiry date.",
31253
31486
  annotations: {
31254
31487
  title: "Get license key",
@@ -31267,6 +31500,7 @@ var licenseKeyTools = [
31267
31500
  },
31268
31501
  {
31269
31502
  name: "ls_list_license_keys",
31503
+ authorityClass: "read",
31270
31504
  description: "List all license keys, optionally filtered by store, order, or product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31271
31505
  annotations: {
31272
31506
  title: "List license keys",
@@ -31295,6 +31529,7 @@ var licenseKeyTools = [
31295
31529
  },
31296
31530
  {
31297
31531
  name: "ls_update_license_key",
31532
+ authorityClass: "key",
31298
31533
  description: "Update a license key's activation limit, expiry date, or disabled status. Setting `disabled: true` revokes customer access and is treated as destructive (rate-limited and audited).",
31299
31534
  annotations: {
31300
31535
  title: "Update license key",
@@ -31337,6 +31572,7 @@ var licenseKeyTools = [
31337
31572
  var licenseTools = [
31338
31573
  {
31339
31574
  name: "ls_activate_license",
31575
+ authorityClass: "key",
31340
31576
  description: "Activate a license key for an instance. Does not require an API key \u2014 uses the license key itself for auth.",
31341
31577
  annotations: {
31342
31578
  title: "Activate license",
@@ -31358,6 +31594,7 @@ var licenseTools = [
31358
31594
  },
31359
31595
  {
31360
31596
  name: "ls_validate_license",
31597
+ authorityClass: "read",
31361
31598
  description: "Validate a license key or specific instance. Does not require an API key \u2014 uses the license key itself for auth.",
31362
31599
  annotations: {
31363
31600
  title: "Validate license",
@@ -31378,6 +31615,7 @@ var licenseTools = [
31378
31615
  },
31379
31616
  {
31380
31617
  name: "ls_deactivate_license",
31618
+ authorityClass: "key",
31381
31619
  description: "Deactivate a license key instance. Does not require an API key \u2014 uses the license key itself for auth.",
31382
31620
  annotations: {
31383
31621
  title: "Deactivate license",
@@ -31403,6 +31641,7 @@ var licenseTools = [
31403
31641
  var orderItemTools = [
31404
31642
  {
31405
31643
  name: "ls_get_order_item",
31644
+ authorityClass: "read",
31406
31645
  description: "Get a specific order item by ID, including product name, variant, price, and quantity.",
31407
31646
  annotations: {
31408
31647
  title: "Get order item",
@@ -31419,6 +31658,7 @@ var orderItemTools = [
31419
31658
  },
31420
31659
  {
31421
31660
  name: "ls_list_order_items",
31661
+ authorityClass: "read",
31422
31662
  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.",
31423
31663
  annotations: {
31424
31664
  title: "List order items",
@@ -31444,6 +31684,7 @@ var orderItemTools = [
31444
31684
  var orderTools = [
31445
31685
  {
31446
31686
  name: "ls_get_order",
31687
+ authorityClass: "read",
31447
31688
  description: "Get a specific order by ID, including status, total, currency, customer info, and payment details.",
31448
31689
  annotations: {
31449
31690
  title: "Get order",
@@ -31462,6 +31703,7 @@ var orderTools = [
31462
31703
  },
31463
31704
  {
31464
31705
  name: "ls_list_orders",
31706
+ authorityClass: "read",
31465
31707
  description: "List all orders, optionally filtered by store or user email. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31466
31708
  annotations: {
31467
31709
  title: "List orders",
@@ -31483,6 +31725,7 @@ var orderTools = [
31483
31725
  },
31484
31726
  {
31485
31727
  name: "ls_generate_order_invoice",
31728
+ authorityClass: "mutate",
31486
31729
  description: "Generate a PDF invoice for an order. Returns a download URL for the invoice.",
31487
31730
  annotations: {
31488
31731
  title: "Generate order invoice",
@@ -31518,6 +31761,7 @@ var orderTools = [
31518
31761
  },
31519
31762
  {
31520
31763
  name: "ls_refund_order",
31764
+ authorityClass: "money",
31521
31765
  description: "Issue a refund for an order. This is irreversible \u2014 the refund amount is in cents (e.g. 1000 = $10.00).",
31522
31766
  annotations: {
31523
31767
  title: "Refund order",
@@ -31543,6 +31787,7 @@ var orderTools = [
31543
31787
  var priceTools = [
31544
31788
  {
31545
31789
  name: "ls_get_price",
31790
+ authorityClass: "read",
31546
31791
  description: "Get a specific price by ID, including amount, currency, and billing interval.",
31547
31792
  annotations: {
31548
31793
  title: "Get price",
@@ -31559,6 +31804,7 @@ var priceTools = [
31559
31804
  },
31560
31805
  {
31561
31806
  name: "ls_list_prices",
31807
+ authorityClass: "read",
31562
31808
  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.",
31563
31809
  annotations: {
31564
31810
  title: "List prices",
@@ -31582,6 +31828,7 @@ var priceTools = [
31582
31828
  var productTools = [
31583
31829
  {
31584
31830
  name: "ls_get_product",
31831
+ authorityClass: "read",
31585
31832
  description: "Get a specific product by ID, including name, description, price, and status.",
31586
31833
  annotations: {
31587
31834
  title: "Get product",
@@ -31598,6 +31845,7 @@ var productTools = [
31598
31845
  },
31599
31846
  {
31600
31847
  name: "ls_list_products",
31848
+ authorityClass: "read",
31601
31849
  description: "List all products, optionally filtered by store. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31602
31850
  annotations: {
31603
31851
  title: "List products",
@@ -31620,6 +31868,7 @@ var productTools = [
31620
31868
  var storeTools = [
31621
31869
  {
31622
31870
  name: "ls_get_store",
31871
+ authorityClass: "read",
31623
31872
  description: "Get a specific store by ID, including name, slug, currency, and sales statistics.",
31624
31873
  annotations: {
31625
31874
  title: "Get store",
@@ -31638,6 +31887,7 @@ var storeTools = [
31638
31887
  },
31639
31888
  {
31640
31889
  name: "ls_list_stores",
31890
+ authorityClass: "read",
31641
31891
  description: "List all stores for the authenticated user. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31642
31892
  annotations: {
31643
31893
  title: "List stores",
@@ -31661,6 +31911,7 @@ var storeTools = [
31661
31911
  var subscriptionInvoiceTools = [
31662
31912
  {
31663
31913
  name: "ls_get_subscription_invoice",
31914
+ authorityClass: "read",
31664
31915
  description: "Get a specific subscription invoice by ID, including status, total, billing reason, and payment details.",
31665
31916
  annotations: {
31666
31917
  title: "Get subscription invoice",
@@ -31677,6 +31928,7 @@ var subscriptionInvoiceTools = [
31677
31928
  },
31678
31929
  {
31679
31930
  name: "ls_list_subscription_invoices",
31931
+ authorityClass: "read",
31680
31932
  description: "List all subscription invoices, optionally filtered by store, subscription, or status. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31681
31933
  annotations: {
31682
31934
  title: "List subscription invoices",
@@ -31703,6 +31955,7 @@ var subscriptionInvoiceTools = [
31703
31955
  },
31704
31956
  {
31705
31957
  name: "ls_generate_subscription_invoice",
31958
+ authorityClass: "mutate",
31706
31959
  description: "Generate a PDF invoice for a subscription invoice. Returns a download URL.",
31707
31960
  annotations: {
31708
31961
  title: "Generate subscription invoice",
@@ -31740,6 +31993,7 @@ var subscriptionInvoiceTools = [
31740
31993
  },
31741
31994
  {
31742
31995
  name: "ls_refund_subscription_invoice",
31996
+ authorityClass: "money",
31743
31997
  description: "Issue a refund for a subscription invoice. This is irreversible \u2014 the refund amount is in cents (e.g. 1000 = $10.00).",
31744
31998
  annotations: {
31745
31999
  title: "Refund subscription invoice",
@@ -31769,6 +32023,7 @@ var subscriptionInvoiceTools = [
31769
32023
  var subscriptionItemTools = [
31770
32024
  {
31771
32025
  name: "ls_get_subscription_item",
32026
+ authorityClass: "read",
31772
32027
  description: "Get a specific subscription item by ID, including quantity, pricing, and associated subscription.",
31773
32028
  annotations: {
31774
32029
  title: "Get subscription item",
@@ -31785,6 +32040,7 @@ var subscriptionItemTools = [
31785
32040
  },
31786
32041
  {
31787
32042
  name: "ls_list_subscription_items",
32043
+ authorityClass: "read",
31788
32044
  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.",
31789
32045
  annotations: {
31790
32046
  title: "List subscription items",
@@ -31805,6 +32061,7 @@ var subscriptionItemTools = [
31805
32061
  },
31806
32062
  {
31807
32063
  name: "ls_update_subscription_item",
32064
+ authorityClass: "recurring",
31808
32065
  description: "Update a subscription item's quantity. Used for seat-based or quantity-based billing.",
31809
32066
  annotations: {
31810
32067
  title: "Update subscription item",
@@ -31829,6 +32086,7 @@ var subscriptionItemTools = [
31829
32086
  },
31830
32087
  {
31831
32088
  name: "ls_get_subscription_item_usage",
32089
+ authorityClass: "read",
31832
32090
  description: "Get the current usage for a metered subscription item within the current billing period.",
31833
32091
  annotations: {
31834
32092
  title: "Get subscription item usage",
@@ -31850,6 +32108,7 @@ var subscriptionItemTools = [
31850
32108
  var subscriptionTools = [
31851
32109
  {
31852
32110
  name: "ls_get_subscription",
32111
+ authorityClass: "read",
31853
32112
  description: "Get a specific subscription by ID, including status, billing interval, renewal date, and customer info.",
31854
32113
  annotations: {
31855
32114
  title: "Get subscription",
@@ -31868,6 +32127,7 @@ var subscriptionTools = [
31868
32127
  },
31869
32128
  {
31870
32129
  name: "ls_list_subscriptions",
32130
+ authorityClass: "read",
31871
32131
  description: "List all subscriptions, optionally filtered by store, order, product, variant, or status. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
31872
32132
  annotations: {
31873
32133
  title: "List subscriptions",
@@ -31902,6 +32162,7 @@ var subscriptionTools = [
31902
32162
  },
31903
32163
  {
31904
32164
  name: "ls_update_subscription",
32165
+ authorityClass: "recurring",
31905
32166
  description: "Update a subscription. Can change the variant (plan switch), pause/unpause, set billing anchor, or update invoice details. Use ls_cancel_subscription for cancellation.",
31906
32167
  annotations: {
31907
32168
  title: "Update subscription",
@@ -31953,6 +32214,7 @@ var subscriptionTools = [
31953
32214
  },
31954
32215
  {
31955
32216
  name: "ls_cancel_subscription",
32217
+ authorityClass: "recurring",
31956
32218
  description: "Cancel a subscription. The subscription remains active until the end of the current billing period, then expires.",
31957
32219
  annotations: {
31958
32220
  title: "Cancel subscription",
@@ -31974,6 +32236,7 @@ var subscriptionTools = [
31974
32236
  var usageRecordTools = [
31975
32237
  {
31976
32238
  name: "ls_get_usage_record",
32239
+ authorityClass: "read",
31977
32240
  description: "Get a specific usage record by ID, including quantity and action type.",
31978
32241
  annotations: {
31979
32242
  title: "Get usage record",
@@ -31990,6 +32253,7 @@ var usageRecordTools = [
31990
32253
  },
31991
32254
  {
31992
32255
  name: "ls_list_usage_records",
32256
+ authorityClass: "read",
31993
32257
  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.",
31994
32258
  annotations: {
31995
32259
  title: "List usage records",
@@ -32009,6 +32273,7 @@ var usageRecordTools = [
32009
32273
  },
32010
32274
  {
32011
32275
  name: "ls_create_usage_record",
32276
+ authorityClass: "mutate",
32012
32277
  description: "Report usage for a metered subscription item. Use 'increment' action to add to the current usage, or 'set' to replace it.",
32013
32278
  annotations: {
32014
32279
  title: "Create usage record",
@@ -32049,6 +32314,7 @@ var usageRecordTools = [
32049
32314
  var userTools = [
32050
32315
  {
32051
32316
  name: "ls_get_user",
32317
+ authorityClass: "read",
32052
32318
  description: "Get the authenticated user's information including name, email, and avatar.",
32053
32319
  annotations: {
32054
32320
  title: "Get authenticated user",
@@ -32068,6 +32334,7 @@ var userTools = [
32068
32334
  var variantTools = [
32069
32335
  {
32070
32336
  name: "ls_get_variant",
32337
+ authorityClass: "read",
32071
32338
  description: "Get a specific product variant by ID, including price, billing interval, and trial settings.",
32072
32339
  annotations: {
32073
32340
  title: "Get variant",
@@ -32084,6 +32351,7 @@ var variantTools = [
32084
32351
  },
32085
32352
  {
32086
32353
  name: "ls_list_variants",
32354
+ authorityClass: "read",
32087
32355
  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.",
32088
32356
  annotations: {
32089
32357
  title: "List variants",
@@ -32107,6 +32375,7 @@ var variantTools = [
32107
32375
  var webhookTools = [
32108
32376
  {
32109
32377
  name: "ls_get_webhook",
32378
+ authorityClass: "read",
32110
32379
  description: "Get a specific webhook by ID, including URL, events, and last sent timestamp.",
32111
32380
  annotations: {
32112
32381
  title: "Get webhook",
@@ -32123,6 +32392,7 @@ var webhookTools = [
32123
32392
  },
32124
32393
  {
32125
32394
  name: "ls_list_webhooks",
32395
+ authorityClass: "read",
32126
32396
  description: "List all webhooks, optionally filtered by store. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total.",
32127
32397
  annotations: {
32128
32398
  title: "List webhooks",
@@ -32141,6 +32411,7 @@ var webhookTools = [
32141
32411
  },
32142
32412
  {
32143
32413
  name: "ls_create_webhook",
32414
+ authorityClass: "webhook",
32144
32415
  description: "Create a new webhook to receive event notifications. The signing secret is returned only once \u2014 save it immediately.",
32145
32416
  annotations: {
32146
32417
  title: "Create webhook",
@@ -32175,6 +32446,7 @@ var webhookTools = [
32175
32446
  },
32176
32447
  {
32177
32448
  name: "ls_update_webhook",
32449
+ authorityClass: "webhook",
32178
32450
  description: "Update an existing webhook's URL, events, or secret.",
32179
32451
  annotations: {
32180
32452
  title: "Update webhook",
@@ -32205,6 +32477,7 @@ var webhookTools = [
32205
32477
  },
32206
32478
  {
32207
32479
  name: "ls_delete_webhook",
32480
+ authorityClass: "webhook",
32208
32481
  description: "Permanently delete a webhook. This is irreversible.",
32209
32482
  annotations: {
32210
32483
  title: "Delete webhook",
@@ -32223,7 +32496,7 @@ var webhookTools = [
32223
32496
  ];
32224
32497
 
32225
32498
  // src/index.ts
32226
- var version2 = true ? "0.7.1" : (await null).createRequire(import.meta.url)("../package.json").version;
32499
+ var version2 = true ? "0.9.0" : (await null).createRequire(import.meta.url)("../package.json").version;
32227
32500
  var subcommand = process.argv[2];
32228
32501
  if (subcommand === "version" || subcommand === "--version") {
32229
32502
  console.log(version2);
@@ -32266,12 +32539,14 @@ for (const tool of allTools) {
32266
32539
  const isDestructive = isDestructiveCall(tool, input);
32267
32540
  const start = Date.now();
32268
32541
  try {
32542
+ checkClassAllowed(tool.authorityClass);
32543
+ checkClassRateLimit(tool.authorityClass);
32269
32544
  if (isDestructive) checkDestructiveRateLimit();
32270
32545
  checkStoreScopedToolInput(tool, input);
32271
32546
  const result = await tool.handler(input);
32272
32547
  const response = result;
32273
32548
  const latency_ms = Date.now() - start;
32274
- logEvent({
32549
+ const successEntry = {
32275
32550
  event: "tool_call",
32276
32551
  tool: tool.name,
32277
32552
  status: response.ok ? "ok" : "error",
@@ -32280,7 +32555,11 @@ for (const tool of allTools) {
32280
32555
  error: response.ok ? void 0 : response.error,
32281
32556
  audit: isDestructive ? true : void 0,
32282
32557
  inputs: isDestructive ? redactSecrets(input) : void 0
32283
- });
32558
+ };
32559
+ logEvent(successEntry);
32560
+ if (isDestructive) {
32561
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32562
+ }
32284
32563
  if (!response.ok) {
32285
32564
  return {
32286
32565
  content: [
@@ -32299,7 +32578,7 @@ for (const tool of allTools) {
32299
32578
  } catch (err) {
32300
32579
  const message = err instanceof Error ? err.message : String(err);
32301
32580
  const latency_ms = Date.now() - start;
32302
- logEvent({
32581
+ const errorEntry = {
32303
32582
  event: "tool_call",
32304
32583
  tool: tool.name,
32305
32584
  status: err instanceof GuardrailError ? "guardrail_block" : "exception",
@@ -32307,7 +32586,11 @@ for (const tool of allTools) {
32307
32586
  error: message,
32308
32587
  audit: isDestructive ? true : void 0,
32309
32588
  inputs: isDestructive ? redactSecrets(input) : void 0
32310
- });
32589
+ };
32590
+ logEvent(errorEntry);
32591
+ if (isDestructive) {
32592
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32593
+ }
32311
32594
  return {
32312
32595
  content: [{ type: "text", text: `Error: ${message}` }],
32313
32596
  isError: true
@@ -32316,6 +32599,27 @@ for (const tool of allTools) {
32316
32599
  }
32317
32600
  );
32318
32601
  }
32602
+ server.resource(
32603
+ "Recent destructive-call audit log",
32604
+ "lemonsqueezy://audit-log",
32605
+ {
32606
+ description: "The most recent destructive tool calls and their outcomes (rate limit, refund cap, etc.). Bounded ring buffer; resets on server restart.",
32607
+ mimeType: "application/x-ndjson"
32608
+ },
32609
+ async (uri) => {
32610
+ const entries = readAuditEntries();
32611
+ const text = entries.map((e) => JSON.stringify(e)).join("\n");
32612
+ return {
32613
+ contents: [
32614
+ {
32615
+ uri: uri.href,
32616
+ mimeType: "application/x-ndjson",
32617
+ text
32618
+ }
32619
+ ]
32620
+ };
32621
+ }
32622
+ );
32319
32623
  var transport = new StdioServerTransport();
32320
32624
  await server.connect(transport);
32321
32625
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
+ "mcpName": "io.github.YawLabs/lemonsqueezy-mcp",
4
5
  "description": "LemonSqueezy MCP server for managing your store from AI assistants",
5
6
  "license": "MIT",
6
7
  "author": "YawLabs <contact@yaw.sh>",